- modified the way that the start GCode is stored such that now the bug in GCode Editor that did not allowed selection of the first tool is now solved
2615 lines
110 KiB
Python
2615 lines
110 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 appEditors.AppTextEditor import AppTextEditor
|
|
from appObjects.FlatCAMObj import *
|
|
|
|
from matplotlib.backend_bases import KeyEvent as mpl_key_event
|
|
|
|
from camlib import CNCjob
|
|
|
|
from shapely.ops import unary_union
|
|
from shapely.geometry import Point, MultiPoint, Polygon, LineString, box
|
|
import shapely.affinity as affinity
|
|
try:
|
|
from shapely.ops import voronoi_diagram
|
|
VORONOI_ENABLED = True
|
|
# from appCommon.Common import voronoi_diagram
|
|
except Exception:
|
|
VORONOI_ENABLED = False
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import serial
|
|
import glob
|
|
import math
|
|
import numpy as np
|
|
import random
|
|
|
|
import gettext
|
|
import appTranslation 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)
|
|
build_al_table_sig = QtCore.pyqtSignal()
|
|
|
|
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': [], list of dicts 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
|
|
|
|
self.coords_decimals = 4
|
|
self.fr_decimals = 2
|
|
|
|
self.annotations_dict = {}
|
|
|
|
# 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)
|
|
|
|
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.gcode_viewer_tab = None
|
|
|
|
self.source_file = ''
|
|
self.units_found = self.app.defaults['units']
|
|
self.probing_gcode_text = ''
|
|
self.grbl_probe_result = ''
|
|
|
|
# store the current selection shape status to be restored after manual adding test points
|
|
self.old_selection_state = self.app.defaults['global_selection_shape']
|
|
|
|
# if mouse is dragging set the object True
|
|
self.mouse_is_dragging = False
|
|
|
|
# if mouse events are bound to local methods
|
|
self.mouse_events_connected = False
|
|
|
|
# event handlers references
|
|
self.kp = None
|
|
self.mm = None
|
|
self.mr = None
|
|
|
|
self.prepend_snippet = ''
|
|
self.append_snippet = ''
|
|
self.gc_header = self.gcode_header()
|
|
self.gc_start = ''
|
|
self.gc_end = ''
|
|
|
|
'''
|
|
dictionary of dictionaries to store the information's for the autolevelling
|
|
format when using Voronoi diagram:
|
|
{
|
|
id: {
|
|
'point': Shapely Point
|
|
'geo': Shapely Polygon from Voronoi diagram,
|
|
'height': float
|
|
}
|
|
}
|
|
'''
|
|
self.al_voronoi_geo_storage = {}
|
|
|
|
'''
|
|
list of (x, y, x) tuples to store the information's for the autolevelling
|
|
format when using bilinear interpolation:
|
|
[(x0, y0, z0), (x1, y1, z1), ...]
|
|
'''
|
|
self.al_bilinear_geo_storage = []
|
|
|
|
self.solid_geo = None
|
|
self.grbl_ser_port = None
|
|
|
|
self.pressed_button = None
|
|
|
|
if self.app.is_legacy is False:
|
|
self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
|
|
else:
|
|
self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_probing_shapes")
|
|
|
|
# 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', 'append_snippet',
|
|
'prepend_snippet', 'gc_header'
|
|
]
|
|
|
|
def build_ui(self):
|
|
self.ui_disconnect()
|
|
|
|
# FIXME: until Shapely 1.8 comes this is disabled
|
|
self.ui.sal_btn.setChecked(False)
|
|
self.ui.sal_btn.setDisabled(True)
|
|
self.ui.sal_btn.setToolTip("DISABLED. Work in progress!")
|
|
|
|
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()
|
|
|
|
if self.ui.sal_btn.isChecked():
|
|
self.build_al_table()
|
|
|
|
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']))
|
|
try:
|
|
offset_val = self.app.dec_format(float(dia_value['offset']), self.decimals) + self.z_cut
|
|
except KeyError:
|
|
offset_val = self.app.dec_format(float(dia_value['offset_z']), self.decimals) + self.z_cut
|
|
|
|
cutz_item = QtWidgets.QTableWidgetItem('%f' % offset_val)
|
|
|
|
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)
|
|
|
|
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 build_al_table(self):
|
|
tool_idx = 0
|
|
|
|
n = len(self.al_voronoi_geo_storage)
|
|
self.ui.al_probe_points_table.setRowCount(n)
|
|
|
|
for id_key, value in self.al_voronoi_geo_storage.items():
|
|
tool_idx += 1
|
|
row_no = tool_idx - 1
|
|
|
|
t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
|
|
x = value['point'].x
|
|
y = value['point'].y
|
|
xy_coords = self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals)
|
|
coords_item = QtWidgets.QTableWidgetItem(str(xy_coords))
|
|
height = self.app.dec_format(value['height'], dec=self.app.decimals)
|
|
height_item = QtWidgets.QTableWidgetItem(str(height))
|
|
|
|
t_id.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
coords_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
height_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
|
self.ui.al_probe_points_table.setItem(row_no, 0, t_id) # Tool name/id
|
|
self.ui.al_probe_points_table.setItem(row_no, 1, coords_item) # X-Y coords
|
|
self.ui.al_probe_points_table.setItem(row_no, 2, height_item) # Determined Height
|
|
|
|
self.ui.al_probe_points_table.resizeColumnsToContents()
|
|
self.ui.al_probe_points_table.resizeRowsToContents()
|
|
|
|
h_header = self.ui.al_probe_points_table.horizontalHeader()
|
|
h_header.setMinimumSectionSize(10)
|
|
h_header.setDefaultSectionSize(70)
|
|
h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
|
|
h_header.resizeSection(0, 20)
|
|
h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
|
h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
|
|
|
|
self.ui.al_probe_points_table.setMinimumHeight(self.ui.al_probe_points_table.getHeight())
|
|
self.ui.al_probe_points_table.setMaximumHeight(self.ui.al_probe_points_table.getHeight())
|
|
|
|
if self.ui.al_probe_points_table.model().rowCount():
|
|
self.ui.grbl_get_heightmap_button.setDisabled(False)
|
|
self.ui.grbl_save_height_map_button.setDisabled(False)
|
|
self.ui.h_gcode_button.setDisabled(False)
|
|
self.ui.view_h_gcode_button.setDisabled(False)
|
|
else:
|
|
self.ui.grbl_get_heightmap_button.setDisabled(True)
|
|
self.ui.grbl_save_height_map_button.setDisabled(True)
|
|
self.ui.h_gcode_button.setDisabled(True)
|
|
self.ui.view_h_gcode_button.setDisabled(True)
|
|
|
|
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 below
|
|
# 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,
|
|
"al_travelz": self.ui.ptravelz_entry,
|
|
"al_probe_depth": self.ui.pdepth_entry,
|
|
"al_probe_fr": self.ui.feedrate_probe_entry,
|
|
"al_controller": self.ui.al_controller_combo,
|
|
"al_method": self.ui.al_method_radio,
|
|
"al_mode": self.ui.al_mode_radio,
|
|
"al_rows": self.ui.al_rows_entry,
|
|
"al_columns": self.ui.al_columns_entry,
|
|
"al_grbl_jog_step": self.ui.jog_step_entry,
|
|
"al_grbl_jog_fr": self.ui.jog_fr_entry,
|
|
})
|
|
|
|
self.append_snippet = self.app.defaults['cncjob_append']
|
|
self.prepend_snippet = self.app.defaults['cncjob_prepend']
|
|
|
|
if self.append_snippet != '' or self.prepend_snippet != '':
|
|
self.ui.snippets_cb.set_value(True)
|
|
|
|
# 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"])
|
|
|
|
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.review_gcode_button.clicked.connect(self.on_review_code_click)
|
|
|
|
# Editor Signal
|
|
self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
|
|
|
|
# Properties
|
|
self.ui.properties_button.toggled.connect(self.on_properties)
|
|
self.calculations_finished.connect(self.update_area_chull)
|
|
|
|
# autolevelling signals
|
|
self.ui.sal_btn.toggled.connect(self.on_toggle_autolevelling)
|
|
self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
|
|
self.ui.al_method_radio.activated_custom.connect(self.on_method_radio)
|
|
self.ui.al_controller_combo.currentIndexChanged.connect(self.on_controller_change)
|
|
self.ui.plot_probing_pts_cb.stateChanged.connect(self.show_probing_geo)
|
|
# GRBL
|
|
self.ui.com_search_button.clicked.connect(self.on_grbl_search_ports)
|
|
self.ui.add_bd_button.clicked.connect(self.on_grbl_add_baudrate)
|
|
self.ui.del_bd_button.clicked.connect(self.on_grbl_delete_baudrate_grbl)
|
|
self.ui.controller_reset_button.clicked.connect(self.on_grbl_reset)
|
|
self.ui.com_connect_button.clicked.connect(self.on_grbl_connect)
|
|
self.ui.grbl_send_button.clicked.connect(self.on_grbl_send_command)
|
|
self.ui.grbl_command_entry.returnPressed.connect(self.on_grbl_send_command)
|
|
|
|
# Jog
|
|
self.ui.jog_wdg.jog_up_button.clicked.connect(lambda: self.on_grbl_jog(direction='yplus'))
|
|
self.ui.jog_wdg.jog_down_button.clicked.connect(lambda: self.on_grbl_jog(direction='yminus'))
|
|
self.ui.jog_wdg.jog_right_button.clicked.connect(lambda: self.on_grbl_jog(direction='xplus'))
|
|
self.ui.jog_wdg.jog_left_button.clicked.connect(lambda: self.on_grbl_jog(direction='xminus'))
|
|
self.ui.jog_wdg.jog_z_up_button.clicked.connect(lambda: self.on_grbl_jog(direction='zplus'))
|
|
self.ui.jog_wdg.jog_z_down_button.clicked.connect(lambda: self.on_grbl_jog(direction='zminus'))
|
|
self.ui.jog_wdg.jog_origin_button.clicked.connect(lambda: self.on_grbl_jog(direction='origin'))
|
|
|
|
# Zero
|
|
self.ui.zero_axs_wdg.grbl_zerox_button.clicked.connect(lambda: self.on_grbl_zero(axis='x'))
|
|
self.ui.zero_axs_wdg.grbl_zeroy_button.clicked.connect(lambda: self.on_grbl_zero(axis='y'))
|
|
self.ui.zero_axs_wdg.grbl_zeroz_button.clicked.connect(lambda: self.on_grbl_zero(axis='z'))
|
|
self.ui.zero_axs_wdg.grbl_zero_all_button.clicked.connect(lambda: self.on_grbl_zero(axis='all'))
|
|
self.ui.zero_axs_wdg.grbl_homing_button.clicked.connect(self.on_grbl_homing)
|
|
|
|
# Sender
|
|
self.ui.grbl_report_button.clicked.connect(lambda: self.send_grbl_command(command='?'))
|
|
self.ui.grbl_get_param_button.clicked.connect(
|
|
lambda: self.on_grbl_get_parameter(param=self.ui.grbl_parameter_entry.get_value()))
|
|
self.ui.view_h_gcode_button.clicked.connect(self.on_edit_probing_gcode)
|
|
self.ui.h_gcode_button.clicked.connect(self.on_save_probing_gcode)
|
|
self.ui.import_heights_button.clicked.connect(self.on_import_height_map)
|
|
self.ui.pause_resume_button.clicked.connect(self.on_grbl_pause_resume)
|
|
self.ui.grbl_get_heightmap_button.clicked.connect(self.on_grbl_autolevel)
|
|
self.ui.grbl_save_height_map_button.clicked.connect(self.on_grbl_heightmap_save)
|
|
|
|
self.build_al_table_sig.connect(self.build_al_table)
|
|
|
|
# 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)
|
|
|
|
# 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.sal_btn.hide()
|
|
self.ui.sal_btn.setChecked(False)
|
|
else:
|
|
self.ui.level.setText(_(
|
|
'<span style="color:red;"><b>Advanced</b></span>'
|
|
))
|
|
if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name or 'hpgl' in \
|
|
self.pp_geometry_name:
|
|
self.ui.sal_btn.hide()
|
|
self.ui.sal_btn.setChecked(False)
|
|
else:
|
|
self.ui.sal_btn.show()
|
|
self.ui.sal_btn.setChecked(self.app.defaults["cncjob_al_status"])
|
|
|
|
preamble = self.prepend_snippet
|
|
postamble = self.append_snippet
|
|
gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
|
|
self.source_file = gc.getvalue()
|
|
|
|
self.ui.al_mode_radio.set_value(self.options['al_mode'])
|
|
self.on_controller_change()
|
|
|
|
self.on_mode_radio(val=self.options['al_mode'])
|
|
self.on_method_radio(val=self.options['al_method'])
|
|
|
|
# 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)
|
|
for row in range(self.ui.exc_cnc_tools_table.rowCount()):
|
|
self.ui.exc_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)
|
|
|
|
self.ui.al_add_button.clicked.connect(self.on_add_al_probepoints)
|
|
self.ui.show_al_table.stateChanged.connect(self.on_show_al_table)
|
|
|
|
def ui_disconnect(self):
|
|
for row in range(self.ui.cnc_tools_table.rowCount()):
|
|
try:
|
|
self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
for row in range(self.ui.exc_cnc_tools_table.rowCount()):
|
|
try:
|
|
self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
try:
|
|
self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
try:
|
|
self.ui.al_add_button.clicked.disconnect()
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
try:
|
|
self.ui.show_al_table.stateChanged.disconnect()
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
def on_properties(self, state):
|
|
if state:
|
|
self.ui.properties_frame.show()
|
|
else:
|
|
self.ui.properties_frame.hide()
|
|
return
|
|
|
|
self.ui.treeWidget.clear()
|
|
self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
|
|
|
|
self.ui.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.MinimumExpanding)
|
|
# make sure that the FCTree widget columns are resized to content
|
|
self.ui.treeWidget.resize_sig.emit()
|
|
|
|
def on_add_al_probepoints(self):
|
|
# create the solid_geo
|
|
|
|
self.solid_geo = unary_union([geo['geom'] for geo in self.gcode_parsed if geo['kind'][0] == 'C'])
|
|
|
|
# reset al table
|
|
self.ui.al_probe_points_table.setRowCount(0)
|
|
|
|
# reset the al dict
|
|
self.al_voronoi_geo_storage.clear()
|
|
|
|
xmin, ymin, xmax, ymax = self.solid_geo.bounds
|
|
|
|
if self.ui.al_mode_radio.get_value() == 'grid':
|
|
width = abs(xmax - xmin)
|
|
height = abs(ymax - ymin)
|
|
cols = self.ui.al_columns_entry.get_value()
|
|
rows = self.ui.al_rows_entry.get_value()
|
|
|
|
dx = 0 if cols == 1 else width / (cols - 1)
|
|
dy = 0 if rows == 1 else height / (rows - 1)
|
|
|
|
points = []
|
|
new_y = ymin
|
|
for x in range(rows):
|
|
new_x = xmin
|
|
for y in range(cols):
|
|
formatted_point = (
|
|
self.app.dec_format(new_x, self.app.decimals),
|
|
self.app.dec_format(new_y, self.app.decimals)
|
|
)
|
|
points.append(formatted_point)
|
|
new_x += dx
|
|
new_y += dy
|
|
|
|
pt_id = 0
|
|
vor_pts_list = []
|
|
bl_pts_list = []
|
|
for point in points:
|
|
pt_id += 1
|
|
pt = Point(point)
|
|
vor_pts_list.append(pt)
|
|
bl_pts_list.append((point[0], point[1], 0.0))
|
|
new_dict = {
|
|
'point': pt,
|
|
'geo': None,
|
|
'height': 0.0
|
|
}
|
|
self.al_voronoi_geo_storage[pt_id] = deepcopy(new_dict)
|
|
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
if al_method == 'v':
|
|
if VORONOI_ENABLED is True:
|
|
self.generate_voronoi_geometry(pts=vor_pts_list)
|
|
# generate Probing GCode
|
|
self.probing_gcode_text = self.probing_gcode(storage=self.al_voronoi_geo_storage)
|
|
else:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Voronoi function can not be loaded.\n"
|
|
"Shapely >= 1.8 is required"))
|
|
else:
|
|
self.generate_bilinear_geometry(pts=bl_pts_list)
|
|
# generate Probing GCode
|
|
self.probing_gcode_text = self.probing_gcode(storage=self.al_bilinear_geo_storage)
|
|
|
|
self.build_al_table_sig.emit()
|
|
if self.ui.plot_probing_pts_cb.get_value():
|
|
self.show_probing_geo(state=True, reset=True)
|
|
else:
|
|
# clear probe shapes
|
|
self.plot_probing_geo(None, False)
|
|
|
|
else:
|
|
f_probe_pt = Point([xmin, xmin])
|
|
int_keys = [int(k) for k in self.al_voronoi_geo_storage.keys()]
|
|
new_id = max(int_keys) + 1 if int_keys else 1
|
|
new_dict = {
|
|
'point': f_probe_pt,
|
|
'geo': None,
|
|
'height': 0.0
|
|
}
|
|
self.al_voronoi_geo_storage[new_id] = deepcopy(new_dict)
|
|
|
|
radius = 0.3 if self.units == 'MM' else 0.012
|
|
fprobe_pt_buff = f_probe_pt.buffer(radius)
|
|
|
|
self.app.inform.emit(_("Click on canvas to add a Probe Point..."))
|
|
self.app.defaults['global_selection_shape'] = False
|
|
|
|
if self.app.is_legacy is False:
|
|
self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
|
|
else:
|
|
self.app.plotcanvas.graph_event_disconnect(self.app.kp)
|
|
self.app.plotcanvas.graph_event_disconnect(self.app.mp)
|
|
self.app.plotcanvas.graph_event_disconnect(self.app.mr)
|
|
|
|
self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
|
|
self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
|
|
|
|
self.mouse_events_connected = True
|
|
|
|
self.build_al_table_sig.emit()
|
|
if self.ui.plot_probing_pts_cb.get_value():
|
|
self.show_probing_geo(state=True, reset=True)
|
|
else:
|
|
# clear probe shapes
|
|
self.plot_probing_geo(None, False)
|
|
|
|
self.plot_probing_geo(geometry=fprobe_pt_buff, visibility=True, custom_color="#0000FFFA")
|
|
|
|
def show_probing_geo(self, state, reset=False):
|
|
|
|
if reset:
|
|
self.probing_shapes.clear(update=True)
|
|
|
|
points_geo = []
|
|
poly_geo = []
|
|
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
# voronoi diagram
|
|
if al_method == 'v':
|
|
# create the geometry
|
|
radius = 0.1 if self.units == 'MM' else 0.004
|
|
for pt in self.al_voronoi_geo_storage:
|
|
if not self.al_voronoi_geo_storage[pt]['geo']:
|
|
continue
|
|
|
|
p_geo = self.al_voronoi_geo_storage[pt]['point'].buffer(radius)
|
|
s_geo = self.al_voronoi_geo_storage[pt]['geo'].buffer(0.0000001)
|
|
|
|
points_geo.append(p_geo)
|
|
poly_geo.append(s_geo)
|
|
|
|
if not points_geo and not poly_geo:
|
|
return
|
|
|
|
self.plot_probing_geo(geometry=points_geo, visibility=state, custom_color='#000000FF')
|
|
self.plot_probing_geo(geometry=poly_geo, visibility=state)
|
|
# bilinear interpolation
|
|
elif al_method == 'b':
|
|
radius = 0.1 if self.units == 'MM' else 0.004
|
|
for pt in self.al_bilinear_geo_storage:
|
|
|
|
x_pt = pt[0]
|
|
y_pt = pt[1]
|
|
p_geo = Point([x_pt, y_pt]).buffer(radius)
|
|
|
|
if p_geo.is_valid:
|
|
points_geo.append(p_geo)
|
|
|
|
if not points_geo:
|
|
return
|
|
|
|
self.plot_probing_geo(geometry=points_geo, visibility=state, custom_color='#000000FF')
|
|
|
|
def plot_probing_geo(self, geometry, visibility, custom_color=None):
|
|
if visibility:
|
|
if self.app.is_legacy is False:
|
|
def random_color():
|
|
r_color = np.random.rand(4)
|
|
r_color[3] = 0.5
|
|
return r_color
|
|
else:
|
|
def random_color():
|
|
while True:
|
|
r_color = np.random.rand(4)
|
|
r_color[3] = 0.5
|
|
|
|
new_color = '#'
|
|
for idx in range(len(r_color)):
|
|
new_color += '%x' % int(r_color[idx] * 255)
|
|
# do it until a valid color is generated
|
|
# a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha
|
|
# for a total of 9 chars
|
|
if len(new_color) == 9:
|
|
break
|
|
return new_color
|
|
|
|
try:
|
|
# if self.app.is_legacy is False:
|
|
# color = "#0000FFFE"
|
|
# else:
|
|
# color = "#0000FFFE"
|
|
# for sh in points_geo:
|
|
# self.add_probing_shape(shape=sh, color=color, face_color=color, visible=True)
|
|
|
|
edge_color = "#000000FF"
|
|
try:
|
|
for sh in geometry:
|
|
if custom_color is None:
|
|
self.add_probing_shape(shape=sh, color=edge_color, face_color=random_color(), visible=True)
|
|
else:
|
|
self.add_probing_shape(shape=sh, color=custom_color, face_color=custom_color, visible=True)
|
|
except TypeError:
|
|
if custom_color is None:
|
|
self.add_probing_shape(
|
|
shape=geometry, color=edge_color, face_color=random_color(), visible=True)
|
|
else:
|
|
self.add_probing_shape(
|
|
shape=geometry, color=custom_color, face_color=custom_color, visible=True)
|
|
|
|
self.probing_shapes.redraw()
|
|
except (ObjectDeleted, AttributeError):
|
|
self.probing_shapes.clear(update=True)
|
|
except Exception as e:
|
|
log.debug("CNCJobObject.plot_probing_geo() --> %s" % str(e))
|
|
else:
|
|
self.probing_shapes.clear(update=True)
|
|
|
|
def add_probing_shape(self, **kwargs):
|
|
if self.deleted:
|
|
raise ObjectDeleted()
|
|
else:
|
|
key = self.probing_shapes.add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
|
|
return key
|
|
|
|
def generate_voronoi_geometry(self, pts):
|
|
env = self.solid_geo.envelope
|
|
fact = 1 if self.units == 'MM' else 0.039
|
|
env = env.buffer(fact)
|
|
|
|
new_pts = deepcopy(pts)
|
|
try:
|
|
pts_union = MultiPoint(pts)
|
|
voronoi_union = voronoi_diagram(geom=pts_union, envelope=env)
|
|
except Exception as e:
|
|
log.debug("CNCJobObject.generate_voronoi_geometry() --> %s" % str(e))
|
|
for pt_index in range(len(pts)):
|
|
new_pts[pt_index] = affinity.translate(
|
|
new_pts[pt_index], random.random() * 1e-09, random.random() * 1e-09)
|
|
|
|
pts_union = MultiPoint(new_pts)
|
|
try:
|
|
voronoi_union = voronoi_diagram(geom=pts_union, envelope=env)
|
|
except Exception:
|
|
return
|
|
|
|
new_voronoi = []
|
|
for p in voronoi_union:
|
|
new_voronoi.append(p.intersection(env))
|
|
|
|
for pt_key in list(self.al_voronoi_geo_storage.keys()):
|
|
for poly in new_voronoi:
|
|
if self.al_voronoi_geo_storage[pt_key]['point'].within(poly):
|
|
self.al_voronoi_geo_storage[pt_key]['geo'] = poly
|
|
|
|
def generate_bilinear_geometry(self, pts):
|
|
self.al_bilinear_geo_storage = pts
|
|
|
|
# To be called after clicking on the plot.
|
|
def on_mouse_click_release(self, event):
|
|
|
|
if self.app.is_legacy is False:
|
|
event_pos = event.pos
|
|
# event_is_dragging = event.is_dragging
|
|
right_button = 2
|
|
else:
|
|
event_pos = (event.xdata, event.ydata)
|
|
# event_is_dragging = self.app.plotcanvas.is_dragging
|
|
right_button = 3
|
|
|
|
try:
|
|
x = float(event_pos[0])
|
|
y = float(event_pos[1])
|
|
except TypeError:
|
|
return
|
|
event_pos = (x, y)
|
|
|
|
# do paint single only for left mouse clicks
|
|
if event.button == 1:
|
|
pos = self.app.plotcanvas.translate_coords(event_pos)
|
|
|
|
# use the snapped position as reference
|
|
snapped_pos = self.app.geo_editor.snap(pos[0], pos[1])
|
|
|
|
probe_pt = Point(snapped_pos)
|
|
|
|
xxmin, yymin, xxmax, yymax = self.solid_geo.bounds
|
|
box_geo = box(xxmin, yymin, xxmax, yymax)
|
|
if not probe_pt.within(box_geo):
|
|
self.app.inform.emit(_("Point is not within the object area. Choose another point."))
|
|
return
|
|
|
|
int_keys = [int(k) for k in self.al_voronoi_geo_storage.keys()]
|
|
new_id = max(int_keys) + 1 if int_keys else 1
|
|
new_dict = {
|
|
'point': probe_pt,
|
|
'geo': None,
|
|
'height': 0.0
|
|
}
|
|
self.al_voronoi_geo_storage[new_id] = deepcopy(new_dict)
|
|
|
|
# rebuild the al table
|
|
self.build_al_table_sig.emit()
|
|
|
|
radius = 0.3 if self.units == 'MM' else 0.012
|
|
probe_pt_buff = probe_pt.buffer(radius)
|
|
|
|
self.plot_probing_geo(geometry=probe_pt_buff, visibility=True, custom_color="#0000FFFA")
|
|
|
|
self.app.inform.emit(_("Added a Probe Point... Click again to add another or right click to finish ..."))
|
|
|
|
# if RMB then we exit
|
|
elif event.button == right_button and self.mouse_is_dragging is False:
|
|
if self.app.is_legacy is False:
|
|
self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
|
|
else:
|
|
self.app.plotcanvas.graph_event_disconnect(self.kp)
|
|
self.app.plotcanvas.graph_event_disconnect(self.mr)
|
|
|
|
self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
|
|
self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
|
|
self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
|
|
self.app.on_mouse_click_release_over_plot)
|
|
|
|
# signal that the mouse events are disconnected from local methods
|
|
self.mouse_events_connected = False
|
|
|
|
# restore selection
|
|
self.app.defaults['global_selection_shape'] = self.old_selection_state
|
|
|
|
self.app.inform.emit(_("Finished adding Probe Points..."))
|
|
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
if al_method == 'v':
|
|
if VORONOI_ENABLED is True:
|
|
pts_list = []
|
|
for k in self.al_voronoi_geo_storage:
|
|
pts_list.append(self.al_voronoi_geo_storage[k]['point'])
|
|
self.generate_voronoi_geometry(pts=pts_list)
|
|
|
|
self.probing_gcode_text = self.probing_gcode(self.al_voronoi_geo_storage)
|
|
else:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Voronoi function can not be loaded.\n"
|
|
"Shapely >= 1.8 is required"))
|
|
|
|
# rebuild the al table
|
|
self.build_al_table_sig.emit()
|
|
|
|
if self.ui.plot_probing_pts_cb.get_value():
|
|
self.show_probing_geo(state=True, reset=True)
|
|
else:
|
|
# clear probe shapes
|
|
self.plot_probing_geo(None, False)
|
|
|
|
def on_key_press(self, event):
|
|
# events out of the self.app.collection view (it's about Project Tab) are of type int
|
|
if type(event) is int:
|
|
key = event
|
|
# events from the GUI are of type QKeyEvent
|
|
elif type(event) == QtGui.QKeyEvent:
|
|
key = event.key()
|
|
elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
|
|
key = event.key
|
|
key = QtGui.QKeySequence(key)
|
|
|
|
# check for modifiers
|
|
key_string = key.toString().lower()
|
|
if '+' in key_string:
|
|
mod, __, key_text = key_string.rpartition('+')
|
|
if mod.lower() == 'ctrl':
|
|
# modifiers = QtCore.Qt.ControlModifier
|
|
pass
|
|
elif mod.lower() == 'alt':
|
|
# modifiers = QtCore.Qt.AltModifier
|
|
pass
|
|
elif mod.lower() == 'shift':
|
|
# modifiers = QtCore.Qt.ShiftModifier
|
|
pass
|
|
else:
|
|
# modifiers = QtCore.Qt.NoModifier
|
|
pass
|
|
key = QtGui.QKeySequence(key_text)
|
|
# events from Vispy are of type KeyEvent
|
|
else:
|
|
key = event.key
|
|
|
|
# Escape = Deselect All
|
|
if key == QtCore.Qt.Key_Escape or key == 'Escape':
|
|
if self.mouse_events_connected is True:
|
|
self.mouse_events_connected = False
|
|
if self.app.is_legacy is False:
|
|
self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
|
|
else:
|
|
self.app.plotcanvas.graph_event_disconnect(self.kp)
|
|
self.app.plotcanvas.graph_event_disconnect(self.mr)
|
|
|
|
self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
|
|
self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
|
|
self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
|
|
self.app.on_mouse_click_release_over_plot)
|
|
|
|
if self.ui.big_cursor_cb.get_value():
|
|
# restore cursor
|
|
self.app.on_cursor_type(val=self.old_cursor_type)
|
|
# restore selection
|
|
self.app.defaults['global_selection_shape'] = self.old_selection_state
|
|
|
|
# Grid toggle
|
|
if key == QtCore.Qt.Key_G or key == 'G':
|
|
self.app.ui.grid_snap_btn.trigger()
|
|
|
|
# Jump to coords
|
|
if key == QtCore.Qt.Key_J or key == 'J':
|
|
self.app.on_jump_to()
|
|
|
|
def on_toggle_autolevelling(self, state):
|
|
self.ui.al_frame.show() if state else self.ui.al_frame.hide()
|
|
self.app.defaults["cncjob_al_status"] = True if state else False
|
|
|
|
def autolevell_gcode(self):
|
|
pass
|
|
|
|
def autolevell_gcode_line(self, gcode_line):
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
|
|
coords = ()
|
|
|
|
if al_method == 'v':
|
|
self.autolevell_voronoi(gcode_line, coords)
|
|
elif al_method == 'b':
|
|
self.autolevell_bilinear(gcode_line, coords)
|
|
|
|
def autolevell_bilinear(self, gcode_line, coords):
|
|
pass
|
|
|
|
def autolevell_voronoi(self, gcode_line, coords):
|
|
pass
|
|
|
|
def on_show_al_table(self, state):
|
|
self.ui.al_probe_points_table.show() if state else self.ui.al_probe_points_table.hide()
|
|
|
|
def on_mode_radio(self, val):
|
|
# reset al table
|
|
self.ui.al_probe_points_table.setRowCount(0)
|
|
|
|
# reset the al dict
|
|
self.al_voronoi_geo_storage.clear()
|
|
|
|
# reset Voronoi Shapes
|
|
self.probing_shapes.clear(update=True)
|
|
|
|
# build AL table
|
|
self.build_al_table()
|
|
|
|
if val == "manual":
|
|
self.ui.al_rows_entry.setDisabled(True)
|
|
self.ui.al_rows_label.setDisabled(True)
|
|
self.ui.al_columns_entry.setDisabled(True)
|
|
self.ui.al_columns_label.setDisabled(True)
|
|
self.ui.al_method_lbl.setDisabled(True)
|
|
self.ui.al_method_radio.setDisabled(True)
|
|
self.ui.al_method_radio.set_value('v')
|
|
else:
|
|
self.ui.al_rows_entry.setDisabled(False)
|
|
self.ui.al_rows_label.setDisabled(False)
|
|
self.ui.al_columns_entry.setDisabled(False)
|
|
self.ui.al_columns_label.setDisabled(False)
|
|
self.ui.al_method_lbl.setDisabled(False)
|
|
self.ui.al_method_radio.setDisabled(False)
|
|
self.ui.al_method_radio.set_value(self.app.defaults['cncjob_al_method'])
|
|
|
|
def on_method_radio(self, val):
|
|
if val == 'b':
|
|
self.ui.al_columns_entry.setMinimum(2)
|
|
self.ui.al_rows_entry.setMinimum(2)
|
|
else:
|
|
self.ui.al_columns_entry.setMinimum(1)
|
|
self.ui.al_rows_entry.setMinimum(1)
|
|
|
|
def on_controller_change(self):
|
|
if self.ui.al_controller_combo.get_value() == 'GRBL':
|
|
self.ui.h_gcode_button.hide()
|
|
self.ui.view_h_gcode_button.hide()
|
|
|
|
self.ui.import_heights_button.hide()
|
|
self.ui.grbl_frame.show()
|
|
self.on_grbl_search_ports(muted=True)
|
|
else:
|
|
self.ui.h_gcode_button.show()
|
|
self.ui.view_h_gcode_button.show()
|
|
|
|
self.ui.import_heights_button.show()
|
|
self.ui.grbl_frame.hide()
|
|
|
|
# if the is empty then there is a chance that we've added probe points but the GRBL controller was selected
|
|
# therefore no Probing GCode was genrated (it is different for GRBL on how it gets it's Probing GCode
|
|
if not self.probing_gcode_text or self.probing_gcode_text == '':
|
|
# generate Probing GCode
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
storage = self.al_voronoi_geo_storage if al_method == 'v' else self.al_bilinear_geo_storage
|
|
self.probing_gcode_text = self.probing_gcode(storage=storage)
|
|
|
|
@staticmethod
|
|
def on_grbl_list_serial_ports():
|
|
"""
|
|
Lists serial port names.
|
|
From here: https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python
|
|
|
|
:raises EnvironmentError: On unsupported or unknown platforms
|
|
:returns: A list of the serial ports available on the system
|
|
"""
|
|
|
|
if sys.platform.startswith('win'):
|
|
ports = ['COM%s' % (i + 1) for i in range(256)]
|
|
elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
|
|
# this excludes your current terminal "/dev/tty"
|
|
ports = glob.glob('/dev/tty[A-Za-z]*')
|
|
elif sys.platform.startswith('darwin'):
|
|
ports = glob.glob('/dev/tty.*')
|
|
else:
|
|
raise EnvironmentError('Unsupported platform')
|
|
|
|
result = []
|
|
s = serial.Serial()
|
|
|
|
for port in ports:
|
|
s.port = port
|
|
|
|
try:
|
|
s.open()
|
|
s.close()
|
|
result.append(port)
|
|
except (OSError, serial.SerialException):
|
|
# result.append(port + " (in use)")
|
|
pass
|
|
|
|
return result
|
|
|
|
def on_grbl_search_ports(self, muted=None):
|
|
port_list = self.on_grbl_list_serial_ports()
|
|
self.ui.com_list_combo.clear()
|
|
self.ui.com_list_combo.addItems(port_list)
|
|
if muted is not True:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("COM list updated ..."))
|
|
|
|
def on_grbl_connect(self):
|
|
port_name = self.ui.com_list_combo.currentText()
|
|
if " (" in port_name:
|
|
port_name = port_name.rpartition(" (")[0]
|
|
|
|
baudrate = int(self.ui.baudrates_list_combo.currentText())
|
|
|
|
try:
|
|
self.grbl_ser_port = serial.serial_for_url(port_name, baudrate,
|
|
bytesize=serial.EIGHTBITS,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
timeout=0.1,
|
|
xonxoff=False,
|
|
rtscts=False)
|
|
|
|
# Toggle DTR to reset the controller loaded with GRBL (Arduino, ESP32, etc)
|
|
try:
|
|
self.grbl_ser_port.dtr = False
|
|
except IOError:
|
|
pass
|
|
|
|
self.grbl_ser_port.reset_input_buffer()
|
|
|
|
try:
|
|
self.grbl_ser_port.dtr = True
|
|
except IOError:
|
|
pass
|
|
|
|
answer = self.on_grbl_wake()
|
|
answer = ['ok'] # FIXME: hack for development without a GRBL controller connected
|
|
for line in answer:
|
|
if 'ok' in line.lower():
|
|
self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: seagreen;}")
|
|
self.ui.com_connect_button.setText(_("Connected"))
|
|
self.ui.controller_reset_button.setDisabled(False)
|
|
|
|
for idx in range(self.ui.al_toolbar.count()):
|
|
if self.ui.al_toolbar.tabText(idx) == _("Connect"):
|
|
self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('seagreen'))
|
|
if self.ui.al_toolbar.tabText(idx) == _("Control"):
|
|
self.ui.al_toolbar.tabBar.setTabEnabled(idx, True)
|
|
if self.ui.al_toolbar.tabText(idx) == _("Sender"):
|
|
self.ui.al_toolbar.tabBar.setTabEnabled(idx, True)
|
|
|
|
self.app.inform.emit("%s: %s" % (_("Port connected"), port_name))
|
|
return
|
|
|
|
self.grbl_ser_port.close()
|
|
self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to GRBL on port"), port_name))
|
|
|
|
except serial.SerialException:
|
|
self.grbl_ser_port = serial.Serial()
|
|
self.grbl_ser_port.port = port_name
|
|
self.grbl_ser_port.close()
|
|
self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: red;}")
|
|
self.ui.com_connect_button.setText(_("Disconnected"))
|
|
self.ui.controller_reset_button.setDisabled(True)
|
|
|
|
for idx in range(self.ui.al_toolbar.count()):
|
|
if self.ui.al_toolbar.tabText(idx) == _("Connect"):
|
|
self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
|
|
if self.ui.al_toolbar.tabText(idx) == _("Control"):
|
|
self.ui.al_toolbar.tabBar.setTabEnabled(idx, False)
|
|
if self.ui.al_toolbar.tabText(idx) == _("Sender"):
|
|
self.ui.al_toolbar.tabBar.setTabEnabled(idx, False)
|
|
self.app.inform.emit("%s: %s" % (_("Port is connected. Disconnecting"), port_name))
|
|
except Exception:
|
|
self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to port"), port_name))
|
|
|
|
def on_grbl_add_baudrate(self):
|
|
new_bd = str(self.ui.new_baudrate_entry.get_value())
|
|
if int(new_bd) >= 40 and new_bd not in self.ui.baudrates_list_combo.model().stringList():
|
|
self.ui.baudrates_list_combo.addItem(new_bd)
|
|
self.ui.baudrates_list_combo.setCurrentText(new_bd)
|
|
|
|
def on_grbl_delete_baudrate_grbl(self):
|
|
current_idx = self.ui.baudrates_list_combo.currentIndex()
|
|
self.ui.baudrates_list_combo.removeItem(current_idx)
|
|
|
|
def on_grbl_wake(self):
|
|
# Wake up grbl
|
|
self.grbl_ser_port.write("\r\n\r\n".encode('utf-8'))
|
|
# Wait for GRBL controller to initialize
|
|
time.sleep(1)
|
|
|
|
grbl_out = deepcopy(self.grbl_ser_port.readlines())
|
|
self.grbl_ser_port.reset_input_buffer()
|
|
|
|
return grbl_out
|
|
|
|
def on_grbl_send_command(self):
|
|
cmd = self.ui.grbl_command_entry.get_value()
|
|
|
|
# show the Shell Dock
|
|
self.app.ui.shell_dock.show()
|
|
|
|
def worker_task():
|
|
with self.app.proc_container.new(_("Sending GCode...")):
|
|
self.send_grbl_command(command=cmd)
|
|
|
|
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
|
|
|
|
def send_grbl_command(self, command, echo=True):
|
|
"""
|
|
|
|
:param command: GCode command
|
|
:type command: str
|
|
:param echo: if to send a '\n' char after
|
|
:type echo: bool
|
|
:return: the text returned by the GRBL controller after each command
|
|
:rtype: str
|
|
"""
|
|
cmd = command.strip()
|
|
if echo:
|
|
self.app.inform_shell[str, bool].emit(cmd, False)
|
|
|
|
# Send Gcode command to GRBL
|
|
snd = cmd + '\n'
|
|
self.grbl_ser_port.write(snd.encode('utf-8'))
|
|
grbl_out = self.grbl_ser_port.readlines()
|
|
if not grbl_out:
|
|
self.app.inform_shell[str, bool].emit('\t\t\t: No answer\n', False)
|
|
|
|
result = ''
|
|
for line in grbl_out:
|
|
if echo:
|
|
try:
|
|
self.app.inform_shell.emit('\t\t\t: ' + line.decode('utf-8').strip().upper())
|
|
except Exception as e:
|
|
log.debug("CNCJobObject.send_grbl_command() --> %s" % str(e))
|
|
if 'ok' in line:
|
|
result = grbl_out
|
|
|
|
return result
|
|
|
|
def send_grbl_block(self, command, echo=True):
|
|
stripped_cmd = command.strip()
|
|
|
|
for grbl_line in stripped_cmd.split('\n'):
|
|
if echo:
|
|
self.app.inform_shell[str, bool].emit(grbl_line, False)
|
|
|
|
# Send Gcode block to GRBL
|
|
snd = grbl_line + '\n'
|
|
self.grbl_ser_port.write(snd.encode('utf-8'))
|
|
grbl_out = self.grbl_ser_port.readlines()
|
|
|
|
for line in grbl_out:
|
|
if echo:
|
|
try:
|
|
self.app.inform_shell.emit(' : ' + line.decode('utf-8').strip().upper())
|
|
except Exception as e:
|
|
log.debug("CNCJobObject.send_grbl_block() --> %s" % str(e))
|
|
|
|
def on_grbl_get_parameter(self, param):
|
|
if '$' in param:
|
|
param = param.replace('$', '')
|
|
|
|
snd = '$$\n'
|
|
self.grbl_ser_port.write(snd.encode('utf-8'))
|
|
grbl_out = self.grbl_ser_port.readlines()
|
|
for line in grbl_out:
|
|
decoded_line = line.decode('utf-8')
|
|
par = '$%s' % str(param)
|
|
if par in decoded_line:
|
|
result = float(decoded_line.rpartition('=')[2])
|
|
self.app.shell_message("GRBL Parameter: %s = %s" % (str(param), str(result)), show=True)
|
|
return result
|
|
|
|
def on_grbl_jog(self, direction=None):
|
|
if direction is None:
|
|
return
|
|
cmd = ''
|
|
|
|
step = self.ui.jog_step_entry.get_value(),
|
|
feedrate = self.ui.jog_fr_entry.get_value()
|
|
travelz = float(self.app.defaults["cncjob_al_grbl_travelz"])
|
|
|
|
if direction == 'xplus':
|
|
cmd = "$J=G91 %s X%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
if direction == 'xminus':
|
|
cmd = "$J=G91 %s X-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
if direction == 'yplus':
|
|
cmd = "$J=G91 %s Y%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
if direction == 'yminus':
|
|
cmd = "$J=G91 %s Y-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
|
|
if direction == 'zplus':
|
|
cmd = "$J=G91 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
if direction == 'zminus':
|
|
cmd = "$J=G91 %s Z-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
|
|
|
|
if direction == 'origin':
|
|
cmd = "$J=G90 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(travelz), str(feedrate))
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
cmd = "$J=G90 %s X0.0 Y0.0 F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(feedrate))
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
return
|
|
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
|
|
def on_grbl_zero(self, axis):
|
|
current_mode = self.on_grbl_get_parameter('10')
|
|
if current_mode is None:
|
|
return
|
|
|
|
cmd = '$10=0'
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
|
|
if axis == 'x':
|
|
cmd = 'G10 L2 P1 X0'
|
|
elif axis == 'y':
|
|
cmd = 'G10 L2 P1 Y0'
|
|
elif axis == 'z':
|
|
cmd = 'G10 L2 P1 Z0'
|
|
else:
|
|
# all
|
|
cmd = 'G10 L2 P1 X0 Y0 Z0'
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
|
|
# restore previous mode
|
|
cmd = '$10=%d' % int(current_mode)
|
|
self.send_grbl_command(command=cmd, echo=False)
|
|
|
|
def on_grbl_homing(self):
|
|
cmd = '$H'
|
|
self.app.inform.emit("%s" % _("GRBL is doing a home cycle."))
|
|
self.on_grbl_wake()
|
|
self.send_grbl_command(command=cmd)
|
|
|
|
def on_grbl_reset(self):
|
|
cmd = '\x18'
|
|
self.app.inform.emit("%s" % _("GRBL software reset was sent."))
|
|
self.on_grbl_wake()
|
|
self.send_grbl_command(command=cmd)
|
|
|
|
def on_grbl_pause_resume(self, checked):
|
|
if checked is False:
|
|
cmd = '~'
|
|
self.send_grbl_command(command=cmd)
|
|
self.app.inform.emit("%s" % _("GRBL resumed."))
|
|
else:
|
|
cmd = '!'
|
|
self.send_grbl_command(command=cmd)
|
|
self.app.inform.emit("%s" % _("GRBL paused."))
|
|
|
|
def probing_gcode(self, storage):
|
|
"""
|
|
:param storage: either a dict of dicts (voronoi) or a list of tuples (bilinear)
|
|
:return: Probing GCode
|
|
:rtype: str
|
|
"""
|
|
|
|
p_gcode = ''
|
|
header = ''
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
|
|
coords = []
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
if al_method == 'v':
|
|
for id_key, value in storage.items():
|
|
x = value['point'].x
|
|
y = value['point'].y
|
|
coords.append(
|
|
(
|
|
self.app.dec_format(x, dec=self.app.decimals),
|
|
self.app.dec_format(y, dec=self.app.decimals)
|
|
)
|
|
)
|
|
else:
|
|
for pt in storage:
|
|
x = pt[0]
|
|
y = pt[1]
|
|
coords.append(
|
|
(
|
|
self.app.dec_format(x, dec=self.app.decimals),
|
|
self.app.dec_format(y, dec=self.app.decimals)
|
|
)
|
|
)
|
|
|
|
pr_travel = self.ui.ptravelz_entry.get_value()
|
|
probe_fr = self.ui.feedrate_probe_entry.get_value()
|
|
pr_depth = self.ui.pdepth_entry.get_value()
|
|
controller = self.ui.al_controller_combo.get_value()
|
|
|
|
header += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
|
|
(str(self.app.version), str(self.app.version_date)) + '\n'
|
|
|
|
header += '(This is a autolevelling probing GCode.)\n' \
|
|
'(Make sure that before you start the job you first do a zero for all axis.)\n\n'
|
|
|
|
header += '(Name: ' + str(self.options['name']) + ')\n'
|
|
header += '(Type: ' + "Autolevelling Probing GCode " + ')\n'
|
|
|
|
header += '(Units: ' + self.units.upper() + ')\n'
|
|
header += '(Created on ' + time_str + ')\n'
|
|
|
|
# commands
|
|
if controller == 'MACH3':
|
|
probing_command = 'G31'
|
|
# probing_var = '#2002'
|
|
openfile_command = 'M40'
|
|
closefile_command = 'M41'
|
|
elif controller == 'MACH4':
|
|
probing_command = 'G31'
|
|
# probing_var = '#5063'
|
|
openfile_command = 'M40'
|
|
closefile_command = 'M41'
|
|
elif controller == 'LinuxCNC':
|
|
probing_command = 'G38.2'
|
|
# probing_var = '#5422'
|
|
openfile_command = '(PROBEOPEN a_probing_points_file.txt)'
|
|
closefile_command = '(PROBECLOSE)'
|
|
elif controller == 'GRBL':
|
|
# do nothing here because the Probing GCode for GRBL is obtained differently
|
|
return
|
|
else:
|
|
log.debug("CNCJobObject.probing_gcode() -> controller not supported")
|
|
return
|
|
|
|
# #############################################################################################################
|
|
# ########################### GCODE construction ##############################################################
|
|
# #############################################################################################################
|
|
|
|
# header
|
|
p_gcode += header + '\n'
|
|
# supplementary message for LinuxCNC
|
|
if controller == 'LinuxCNC':
|
|
p_gcode += "The file with the stored probing points can be found\n" \
|
|
"in the configuration folder for LinuxCNC.\n" \
|
|
"The name of the file is: a_probing_points_file.txt.\n"
|
|
# units
|
|
p_gcode += 'G21\n' if self.units == 'MM' else 'G20\n'
|
|
# reference mode = absolute
|
|
p_gcode += 'G90\n'
|
|
# open a new file
|
|
p_gcode += openfile_command + '\n'
|
|
# move to safe height (probe travel Z)
|
|
p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
|
|
|
|
# probing points
|
|
for idx, xy_tuple in enumerate(coords, 1): # index starts from 1
|
|
x = xy_tuple[0]
|
|
y = xy_tuple[1]
|
|
# move to probing point
|
|
p_gcode += "G0 X%sY%s\n" % (
|
|
str(self.app.dec_format(x, self.coords_decimals)),
|
|
str(self.app.dec_format(y, self.coords_decimals))
|
|
)
|
|
# do the probing
|
|
p_gcode += "%s Z%s F%s\n" % (
|
|
probing_command,
|
|
str(self.app.dec_format(pr_depth, self.coords_decimals)),
|
|
str(self.app.dec_format(probe_fr, self.fr_decimals)),
|
|
)
|
|
# store in a global numeric variable the value of the detected probe Z
|
|
# I offset the global numeric variable by 500 so it does not conflict with something else
|
|
# temp_var = int(idx + 500)
|
|
# p_gcode += "#%d = %s\n" % (temp_var, probing_var)
|
|
|
|
# move to safe height (probe travel Z)
|
|
p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
|
|
|
|
# close the file
|
|
p_gcode += closefile_command + '\n'
|
|
# finish the GCode
|
|
p_gcode += 'M2'
|
|
|
|
return p_gcode
|
|
|
|
def on_save_probing_gcode(self):
|
|
lines = StringIO(self.probing_gcode_text)
|
|
|
|
_filter_ = self.app.defaults['cncjob_save_filters']
|
|
name = "probing_gcode"
|
|
try:
|
|
dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Code ..."),
|
|
directory=dir_file_to_save,
|
|
ext_filter=_filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Code ..."),
|
|
ext_filter=_filter_)
|
|
|
|
if filename == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
|
|
return
|
|
else:
|
|
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'
|
|
|
|
def on_edit_probing_gcode(self):
|
|
self.app.proc_container.view.set_busy(_("Loading..."))
|
|
|
|
gco = self.probing_gcode_text
|
|
if gco is None or gco == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s...' % _('There is nothing to view'))
|
|
return
|
|
|
|
self.gcode_viewer_tab = AppTextEditor(app=self.app, plain_text=True)
|
|
|
|
# add the tab if it was closed
|
|
self.app.ui.plot_tab_area.addTab(self.gcode_viewer_tab, '%s' % _("Code Viewer"))
|
|
self.gcode_viewer_tab.setObjectName('code_viewer_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("")
|
|
|
|
self.gcode_viewer_tab.code_editor.completer_enable = False
|
|
self.gcode_viewer_tab.buttonRun.hide()
|
|
|
|
# Switch plot_area to CNCJob tab
|
|
self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_viewer_tab)
|
|
|
|
self.gcode_viewer_tab.t_frame.hide()
|
|
# then append the text from GCode to the text editor
|
|
try:
|
|
self.gcode_viewer_tab.load_text(gco, move_to_start=True, clear_text=True)
|
|
except Exception as e:
|
|
log.debug('FlatCAMCNCJob.on_edit_probing_gcode() -->%s' % str(e))
|
|
return
|
|
|
|
self.gcode_viewer_tab.t_frame.show()
|
|
self.app.proc_container.view.set_idle()
|
|
|
|
self.gcode_viewer_tab.buttonSave.hide()
|
|
self.gcode_viewer_tab.buttonOpen.hide()
|
|
self.gcode_viewer_tab.buttonPrint.hide()
|
|
self.gcode_viewer_tab.buttonPreview.hide()
|
|
self.gcode_viewer_tab.buttonReplace.hide()
|
|
self.gcode_viewer_tab.sel_all_cb.hide()
|
|
self.gcode_viewer_tab.entryReplace.hide()
|
|
|
|
self.gcode_viewer_tab.button_update_code.show()
|
|
|
|
# self.gcode_viewer_tab.code_editor.setReadOnly(True)
|
|
|
|
self.gcode_viewer_tab.button_update_code.clicked.connect(self.on_update_probing_gcode)
|
|
|
|
self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Viewer'))
|
|
|
|
def on_update_probing_gcode(self):
|
|
self.probing_gcode_text = self.gcode_viewer_tab.code_editor.toPlainText()
|
|
|
|
def on_import_height_map(self):
|
|
"""
|
|
Import the height map file into the app
|
|
:return:
|
|
:rtype:
|
|
"""
|
|
|
|
_filter_ = "Text File .txt (*.txt);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
|
|
filter=_filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
self.app.worker_task.emit({'fcn': self.import_height_map, 'params': [filename]})
|
|
|
|
def import_height_map(self, filename):
|
|
"""
|
|
|
|
:param filename:
|
|
:type filename:
|
|
:return:
|
|
:rtype:
|
|
"""
|
|
|
|
try:
|
|
if filename:
|
|
with open(filename, 'r') as f:
|
|
stream = f.readlines()
|
|
else:
|
|
return
|
|
except IOError:
|
|
log.error("Failed to open height map file: %s" % filename)
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open height map file"), filename))
|
|
return
|
|
|
|
idx = 0
|
|
if stream is not None and stream != '':
|
|
for line in stream:
|
|
if line != '':
|
|
idx += 1
|
|
line = line.replace(' ', ',').replace('\n', '').split(',')
|
|
if idx not in self.al_voronoi_geo_storage:
|
|
self.al_voronoi_geo_storage[idx] = {}
|
|
self.al_voronoi_geo_storage[idx]['height'] = float(line[2])
|
|
if 'point' not in self.al_voronoi_geo_storage[idx]:
|
|
x = float(line[0])
|
|
y = float(line[1])
|
|
self.al_voronoi_geo_storage[idx]['point'] = Point((x, y))
|
|
|
|
self.build_al_table_sig.emit()
|
|
|
|
def on_grbl_autolevel(self):
|
|
# show the Shell Dock
|
|
self.app.ui.shell_dock.show()
|
|
|
|
def worker_task():
|
|
with self.app.proc_container.new(_("Sending GCode...")):
|
|
self.grbl_probe_result = ''
|
|
pr_travelz = str(self.ui.ptravelz_entry.get_value())
|
|
probe_fr = str(self.ui.feedrate_probe_entry.get_value())
|
|
pr_depth = str(self.ui.pdepth_entry.get_value())
|
|
|
|
cmd = 'G21\n'
|
|
self.send_grbl_command(command=cmd)
|
|
cmd = 'G90\n'
|
|
self.send_grbl_command(command=cmd)
|
|
|
|
for pt_key in self.al_voronoi_geo_storage:
|
|
x = str(self.al_voronoi_geo_storage[pt_key]['point'].x)
|
|
y = str(self.al_voronoi_geo_storage[pt_key]['point'].y)
|
|
|
|
cmd = 'G0 Z%s\n' % pr_travelz
|
|
self.send_grbl_command(command=cmd)
|
|
cmd = 'G0 X%s Y%s\n' % (x, y)
|
|
self.send_grbl_command(command=cmd)
|
|
cmd = 'G38.2 Z%s F%s' % (pr_depth, probe_fr)
|
|
output = self.send_grbl_command(command=cmd)
|
|
|
|
self.grbl_probe_result += output + '\n'
|
|
|
|
cmd = 'M2\n'
|
|
self.send_grbl_command(command=cmd)
|
|
self.app.inform.emit('%s' % _("Finished probing. Doing the autolevelling."))
|
|
|
|
# apply autolevel here
|
|
self.on_grbl_apply_autolevel()
|
|
|
|
self.app.inform.emit('%s' % _("Sending probing GCode to the GRBL controller."))
|
|
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
|
|
|
|
def on_grbl_heightmap_save(self):
|
|
if self.grbl_probe_result != '':
|
|
_filter_ = "Text File .txt (*.txt);;All Files (*.*)"
|
|
name = "probing_gcode"
|
|
try:
|
|
dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Code ..."),
|
|
directory=dir_file_to_save,
|
|
ext_filter=_filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Code ..."),
|
|
ext_filter=_filter_)
|
|
|
|
if filename == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
|
|
return
|
|
else:
|
|
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 self.grbl_probe_result:
|
|
f.write(line)
|
|
else:
|
|
with open(filename, 'w') as f:
|
|
for line in self.grbl_probe_result:
|
|
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'
|
|
else:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Empty GRBL heightmap."))
|
|
|
|
def on_grbl_apply_autolevel(self):
|
|
# TODO here we call the autolevell method
|
|
self.app.inform.emit('%s' % _("Finished autolevelling."))
|
|
|
|
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):
|
|
"""
|
|
Handler activated by a button clicked when exporting GCode.
|
|
|
|
:param args:
|
|
:return:
|
|
"""
|
|
self.app.defaults.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 Code ..."),
|
|
directory=dir_file_to_save,
|
|
ext_filter=_filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Code ..."),
|
|
ext_filter=_filter_)
|
|
|
|
self.export_gcode_handler(filename, is_gcode=save_gcode)
|
|
|
|
def export_gcode_handler(self, filename, is_gcode=True):
|
|
preamble = ''
|
|
postamble = ''
|
|
filename = str(filename)
|
|
|
|
if filename == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
|
|
return
|
|
else:
|
|
if is_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)
|
|
|
|
try:
|
|
if self.ui.snippets_cb.get_value():
|
|
preamble = self.prepend_snippet
|
|
postamble = self.append_snippet
|
|
gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
|
|
except Exception as err:
|
|
log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err))
|
|
gc = self.export_gcode(filename)
|
|
|
|
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' % (_("File saved to"), filename))
|
|
|
|
def on_review_code_click(self):
|
|
"""
|
|
Handler activated by a button clicked when reviewing GCode.
|
|
|
|
:return:
|
|
"""
|
|
|
|
self.app.proc_container.view.set_busy(_("Loading..."))
|
|
|
|
preamble = self.prepend_snippet
|
|
postamble = self.append_snippet
|
|
|
|
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 = AppTextEditor(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 Review"))
|
|
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("")
|
|
|
|
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.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True)
|
|
except Exception as e:
|
|
log.debug('FlatCAMCNCJob.on_review_code_click() -->%s' % str(e))
|
|
return
|
|
|
|
self.gcode_editor_tab.t_frame.show()
|
|
self.app.proc_container.view.set_idle()
|
|
|
|
self.gcode_editor_tab.buttonSave.hide()
|
|
self.gcode_editor_tab.buttonOpen.hide()
|
|
# self.gcode_editor_tab.buttonPrint.hide()
|
|
# self.gcode_editor_tab.buttonPreview.hide()
|
|
self.gcode_editor_tab.buttonReplace.hide()
|
|
self.gcode_editor_tab.sel_all_cb.hide()
|
|
self.gcode_editor_tab.entryReplace.hide()
|
|
self.gcode_editor_tab.code_editor.setReadOnly(True)
|
|
|
|
self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
|
|
|
|
def on_update_source_file(self):
|
|
self.source_file = self.gcode_editor_tab.code_editor.toPlainText()
|
|
|
|
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
|
|
gcode = ''
|
|
|
|
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
|
|
|
|
if preamble == '':
|
|
preamble = self.app.defaults["cncjob_prepend"]
|
|
if postamble == '':
|
|
postamble = self.app.defaults["cncjob_append"]
|
|
|
|
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']['tools_drill_ppname_e']
|
|
].include_header
|
|
|
|
gcode = ''
|
|
if include_header is False:
|
|
# 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 = preamble + '\n' + gcode + '\n' + 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:
|
|
if self.origin_kind == 'excellon':
|
|
for tooluid_key in self.exc_cnc_tools:
|
|
for key, value in self.exc_cnc_tools[tooluid_key].items():
|
|
if key == 'gcode' and value:
|
|
gcode += value
|
|
break
|
|
else:
|
|
for tooluid_key in self.cnc_tools:
|
|
for key, value in self.cnc_tools[tooluid_key].items():
|
|
if key == 'gcode' and value:
|
|
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_body_gcode = ''
|
|
pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
|
|
|
|
# process body gcode
|
|
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_body_gcode += new_line
|
|
else:
|
|
processed_body_gcode += gline + '\n'
|
|
|
|
gcode = processed_body_gcode
|
|
g = self.gc_header + '\n' + self.gc_start + '\n' + preamble + '\n' + \
|
|
gcode + '\n' + postamble + end_gcode
|
|
else:
|
|
# try:
|
|
# g_idx = gcode.index('G94')
|
|
# if preamble != '' and postamble != '':
|
|
# g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
|
|
# gcode[(g_idx + 3):] + postamble + end_gcode
|
|
# elif preamble == '':
|
|
# g = self.gc_header + gcode[:g_idx + 3] + '\n' + \
|
|
# gcode[(g_idx + 3):] + postamble + end_gcode
|
|
# elif postamble == '':
|
|
# g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
|
|
# gcode[(g_idx + 3):] + end_gcode
|
|
# else:
|
|
# g = self.gc_header + gcode[:g_idx + 3] + gcode[(g_idx + 3):] + end_gcode
|
|
# except ValueError:
|
|
# self.app.inform.emit('[ERROR_NOTCL] %s' %
|
|
# _("G-code does not have a G94 code.\n"
|
|
# "Append Code snippet will not be used.."))
|
|
# g = self.gc_header + '\n' + gcode + postamble + end_gcode
|
|
if preamble != '' and postamble != '':
|
|
g = self.gc_header + self.gc_start + '\n' + preamble + '\n' + gcode + '\n' + \
|
|
postamble + '\n' + end_gcode
|
|
if preamble == '':
|
|
g = self.gc_header + self.gc_start + '\n' + gcode + '\n' + postamble + '\n' + end_gcode
|
|
if postamble == '':
|
|
g = self.gc_header + self.gc_start + '\n' + preamble + '\n' + gcode + '\n' + end_gcode
|
|
if preamble == '' and postamble == '':
|
|
g = self.gc_header + self.gc_start + '\n' + gcode + '\n' + 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():
|
|
# 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)
|
|
if self.origin_kind == "excellon":
|
|
for r in range(self.ui.exc_cnc_tools_table.rowCount()):
|
|
row_dia = float('%.*f' % (self.decimals, float(self.ui.exc_cnc_tools_table.item(r, 1).text())))
|
|
for tooluid_key in self.exc_cnc_tools:
|
|
tooldia = float('%.*f' % (self.decimals, float(tooluid_key)))
|
|
if row_dia == tooldia:
|
|
gcode_parsed = self.exc_cnc_tools[tooluid_key]['gcode_parsed']
|
|
if self.ui.exc_cnc_tools_table.cellWidget(r, 6).isChecked():
|
|
self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
|
|
else:
|
|
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']
|
|
|
|
# Geometry shapes plotting
|
|
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(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
|
|
else:
|
|
# I do this so the travel lines thickness will reflect the tool diameter
|
|
# may work only for objects created within the app and not Gcode imported from elsewhere for which we
|
|
# don't know the origin
|
|
if self.origin_kind == "excellon":
|
|
if self.exc_cnc_tools:
|
|
for tooldia_key in self.exc_cnc_tools:
|
|
tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
|
|
gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
|
|
if not gcode_parsed:
|
|
continue
|
|
# gcode_parsed = self.gcode_parsed
|
|
self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, 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)
|
|
|
|
self.shapes.redraw()
|
|
except (ObjectDeleted, AttributeError):
|
|
self.shapes.clear(update=True)
|
|
if self.app.is_legacy is False:
|
|
self.annotation.clear(update=True)
|
|
|
|
# Annotaions shapes plotting
|
|
try:
|
|
if self.app.is_legacy is False:
|
|
if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
|
|
self.plot_annotations(obj=self, visible=True)
|
|
else:
|
|
self.plot_annotations(obj=self, visible=False)
|
|
|
|
except (ObjectDeleted, AttributeError):
|
|
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'] = unary_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)
|