- Levelling Tool: when adding a Grid probing and the avoidance of Excellon is used, now the probing locations will be offset enough so the probing is not done in the Excellon holes
2532 lines
105 KiB
Python
2532 lines
105 KiB
Python
# ##########################################################
|
|
# FlatCAM Evo: 2D Post-processing for Manufacturing #
|
|
# File by: Marius Adrian Stanciu (c) #
|
|
# Date: 11/12/2020 #
|
|
# License: MIT Licence #
|
|
# ##########################################################
|
|
|
|
from PyQt6 import QtWidgets, QtCore, QtGui
|
|
from PyQt6.QtCore import Qt
|
|
from appTool import AppTool
|
|
from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, FCCheckBox, \
|
|
FCJog, RadioSet, FCDoubleSpinner, FCSpinner, FCFileSaveDialog, FCDetachableTab, FCTable, \
|
|
FCZeroAxes, FCSliderWithDoubleSpinner, FCEntry, RotatedToolButton
|
|
|
|
import logging
|
|
from copy import deepcopy
|
|
import sys
|
|
|
|
from shapely import Point, MultiPoint, MultiPolygon, box
|
|
from shapely.ops import unary_union
|
|
from shapely.affinity import translate
|
|
from datetime import datetime as dt
|
|
|
|
import gettext
|
|
import appTranslation as fcTranslate
|
|
import builtins
|
|
|
|
from appObjects.AppObjectTemplate import ObjectDeleted
|
|
from appGUI.VisPyVisuals import *
|
|
from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
|
|
from appEditors.AppTextEditor import AppTextEditor
|
|
|
|
from camlib import CNCjob
|
|
|
|
import time
|
|
import serial
|
|
import glob
|
|
import random
|
|
from io import StringIO
|
|
|
|
from matplotlib.backend_bases import KeyEvent as mpl_key_event
|
|
|
|
# try:
|
|
# from foronoi import Voronoi
|
|
# from foronoi import Polygon as voronoi_poly
|
|
# VORONOI_ENABLED = True
|
|
# except Exception:
|
|
# try:
|
|
# from shapely.ops import voronoi_diagram
|
|
# VORONOI_ENABLED = True
|
|
# # from appCommon.Common import voronoi_diagram
|
|
# except Exception:
|
|
# VORONOI_ENABLED = False
|
|
try:
|
|
from shapely.ops import voronoi_diagram
|
|
VORONOI_ENABLED = True
|
|
# from appCommon.Common import voronoi_diagram
|
|
except Exception:
|
|
VORONOI_ENABLED = False
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
log = logging.getLogger('base')
|
|
|
|
|
|
class ToolLevelling(AppTool, CNCjob):
|
|
build_al_table_sig = QtCore.pyqtSignal()
|
|
|
|
def __init__(self, app):
|
|
self.app = app
|
|
self.decimals = self.app.decimals
|
|
|
|
AppTool.__init__(self, app)
|
|
CNCjob.__init__(self, steps_per_circle=self.app.options["cncjob_steps_per_circle"])
|
|
|
|
# updated in the self.set_tool_ui()
|
|
self.form_fields = {}
|
|
|
|
self.first_click = False
|
|
self.cursor_pos = None
|
|
|
|
# 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.probing_gcode_text = ''
|
|
self.grbl_probe_result = ''
|
|
|
|
'''
|
|
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.probing_shapes = None
|
|
|
|
self.gcode_viewer_tab = None
|
|
|
|
# store the current selection shape status to be restored after manual adding test points
|
|
self.old_selection_state = self.app.options['global_selection_shape']
|
|
|
|
# #############################################################################
|
|
# ######################### Tool GUI ##########################################
|
|
# #############################################################################
|
|
self.ui = LevelUI(layout=self.layout, app=self.app)
|
|
self.pluginName = self.ui.pluginName
|
|
|
|
self.connect_signals_at_init()
|
|
|
|
def install(self, icon=None, separator=None, **kwargs):
|
|
AppTool.install(self, icon, separator, shortcut='', **kwargs)
|
|
|
|
def run(self, toggle=True):
|
|
self.app.defaults.report_usage("ToolLevelling()")
|
|
|
|
if toggle:
|
|
# if the splitter is hidden, display it
|
|
if self.app.ui.splitter.sizes()[0] == 0:
|
|
self.app.ui.splitter.setSizes([1, 1])
|
|
|
|
# if the Tool Tab is hidden display it, else hide it but only if the objectName is the same
|
|
found_idx = None
|
|
for idx in range(self.app.ui.notebook.count()):
|
|
if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab":
|
|
found_idx = idx
|
|
break
|
|
# show the Tab
|
|
if not found_idx:
|
|
try:
|
|
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
|
|
except RuntimeError:
|
|
self.app.ui.plugin_tab = QtWidgets.QWidget()
|
|
self.app.ui.plugin_tab.setObjectName("plugin_tab")
|
|
self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
|
|
self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
|
|
|
|
self.app.ui.plugin_scroll_area = VerticalScrollArea()
|
|
self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
|
|
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
|
|
# focus on Tool Tab
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
|
|
|
|
try:
|
|
if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx:
|
|
# if the Tool Tab is not focused, focus on it
|
|
if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab:
|
|
# focus on Tool Tab
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
|
|
else:
|
|
# else remove the Tool Tab
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
|
|
self.app.ui.notebook.removeTab(2)
|
|
|
|
# if there are no objects loaded in the app then hide the Notebook widget
|
|
if not self.app.collection.get_list():
|
|
self.app.ui.splitter.setSizes([0, 1])
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if self.app.ui.splitter.sizes()[0] == 0:
|
|
self.app.ui.splitter.setSizes([1, 1])
|
|
|
|
super().run()
|
|
self.set_tool_ui()
|
|
|
|
self.app.ui.notebook.setTabText(2, _("Levelling"))
|
|
|
|
def connect_signals_at_init(self):
|
|
self.build_al_table_sig.connect(self.build_al_table)
|
|
self.ui.level.toggled.connect(self.on_level_changed)
|
|
|
|
self.ui.avoid_exc_holes_cb.toggled.connect(self.on_avoid_exc_holes)
|
|
|
|
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.toggled.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)
|
|
|
|
# When object selection on canvas change
|
|
self.app.proj_selection_changed.connect(self.on_object_selection_changed)
|
|
|
|
# Reset Tool
|
|
self.ui.reset_button.clicked.connect(self.set_tool_ui)
|
|
# Cleanup on Graceful exit (CTRL+ALT+X combo key)
|
|
self.app.cleanup.connect(self.set_tool_ui)
|
|
|
|
def set_tool_ui(self):
|
|
self.units = self.app.app_units.upper()
|
|
|
|
self.clear_ui(self.layout)
|
|
self.ui = LevelUI(layout=self.layout, app=self.app)
|
|
self.pluginName = self.ui.pluginName
|
|
self.connect_signals_at_init()
|
|
|
|
# try to select in the CNCJob combobox the active object
|
|
try:
|
|
selected_obj = self.app.collection.get_active()
|
|
if selected_obj.kind == 'cncjob':
|
|
current_name = selected_obj.obj_options['name']
|
|
self.ui.object_combo.set_value(current_name)
|
|
except Exception:
|
|
pass
|
|
|
|
loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
|
|
if loaded_obj and loaded_obj.kind == 'cncjob':
|
|
name = loaded_obj.obj_options['name']
|
|
else:
|
|
name = ''
|
|
|
|
# Shapes container for the Voronoi cells in Autolevelling
|
|
if self.app.use_3d_engine:
|
|
self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1, pool=self.app.pool)
|
|
else:
|
|
self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_probing_shapes")
|
|
|
|
self.form_fields.update({
|
|
"tools_al_probe_tip_dia": self.ui.probe_tip_dia_entry,
|
|
"tools_al_travel_z": self.ui.ptravelz_entry,
|
|
"tools_al_probe_depth": self.ui.pdepth_entry,
|
|
"tools_al_probe_fr": self.ui.feedrate_probe_entry,
|
|
"tools_al_controller": self.ui.al_controller_combo,
|
|
"tools_al_method": self.ui.al_method_radio,
|
|
"tools_al_mode": self.ui.al_mode_radio,
|
|
"tools_al_avoid_exc_holes_size": self.ui.avoid_exc_holes_size_entry,
|
|
"tools_al_rows": self.ui.al_rows_entry,
|
|
"tools_al_columns": self.ui.al_columns_entry,
|
|
"tools_al_grbl_jog_step": self.ui.jog_step_entry,
|
|
"tools_al_grbl_jog_fr": self.ui.jog_fr_entry,
|
|
})
|
|
|
|
# Fill Form fields
|
|
self.to_form()
|
|
self.on_controller_change_alter_ui()
|
|
|
|
self.ui.plot_probing_pts_cb.set_value(self.app.options["tools_al_plot_points"])
|
|
self.ui.avoid_exc_holes_cb.set_value(self.app.options["tools_al_avoid_exc_holes"])
|
|
|
|
self.ui.al_probe_points_table.setRowCount(0)
|
|
self.ui.al_probe_points_table.resizeColumnsToContents()
|
|
self.ui.al_probe_points_table.resizeRowsToContents()
|
|
v_header = self.ui.al_probe_points_table.verticalHeader()
|
|
v_header.hide()
|
|
self.ui.al_probe_points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
h_header = self.ui.al_probe_points_table.horizontalHeader()
|
|
h_header.setMinimumSectionSize(10)
|
|
h_header.setDefaultSectionSize(70)
|
|
h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed)
|
|
h_header.resizeSection(0, 20)
|
|
h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
|
h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.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())
|
|
|
|
# Set initial UI
|
|
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.set_value('v')
|
|
self.ui.al_method_radio.setDisabled(True)
|
|
|
|
# Show/Hide Advanced Options
|
|
app_mode = self.app.options["global_app_level"]
|
|
self.change_level(app_mode)
|
|
|
|
try:
|
|
self.ui.object_combo.currentIndexChanged.disconnect()
|
|
except (AttributeError, TypeError):
|
|
pass
|
|
self.ui.object_combo.currentIndexChanged.connect(self.on_object_changed)
|
|
|
|
self.build_tool_ui()
|
|
|
|
if loaded_obj and loaded_obj.is_segmented_gcode is True and loaded_obj.obj_options["type"] == 'Geometry':
|
|
self.ui.al_frame.setDisabled(False)
|
|
self.ui.al_mode_radio.set_value(loaded_obj.obj_options['tools_al_mode'])
|
|
self.on_controller_change()
|
|
|
|
self.on_mode_radio(val=loaded_obj.obj_options['tools_al_mode'])
|
|
self.on_method_radio(val=loaded_obj.obj_options['tools_al_method'])
|
|
else:
|
|
self.ui.al_frame.setDisabled(True)
|
|
|
|
self.on_avoid_exc_holes(self.app.options["tools_al_avoid_exc_holes"])
|
|
|
|
def on_object_changed(self):
|
|
|
|
# load the object
|
|
obj_name = self.ui.object_combo.currentText()
|
|
|
|
# Get source object.
|
|
try:
|
|
target_obj = self.app.collection.get_by_name(obj_name)
|
|
except Exception:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
|
|
return
|
|
|
|
if target_obj is not None and target_obj.is_segmented_gcode is True and \
|
|
target_obj.obj_options["type"] == 'Geometry':
|
|
|
|
self.ui.al_frame.setDisabled(False)
|
|
|
|
# Shapes container for the Voronoi cells in Autolevelling
|
|
if self.app.use_3d_engine:
|
|
self.probing_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1,
|
|
pool=self.app.pool)
|
|
else:
|
|
self.probing_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=obj_name + "_probing_shapes")
|
|
else:
|
|
self.ui.al_frame.setDisabled(True)
|
|
|
|
def on_object_selection_changed(self, current, previous):
|
|
found_idx = None
|
|
for tab_idx in range(self.app.ui.notebook.count()):
|
|
if self.app.ui.notebook.tabText(tab_idx) == self.ui.pluginName:
|
|
found_idx = True
|
|
break
|
|
|
|
if found_idx:
|
|
try:
|
|
sel_obj = current.indexes()[0].internalPointer().obj
|
|
name = sel_obj.obj_options['name']
|
|
kind = sel_obj.kind
|
|
|
|
if kind == 'cncjob':
|
|
self.ui.object_combo.set_value(name)
|
|
except IndexError:
|
|
pass
|
|
|
|
def change_level(self, level):
|
|
"""
|
|
|
|
:param level: application level: either 'b' or 'a'
|
|
:type level: str
|
|
:return:
|
|
"""
|
|
|
|
if level == 'a':
|
|
self.ui.level.setChecked(True)
|
|
else:
|
|
self.ui.level.setChecked(False)
|
|
self.on_level_changed(self.ui.level.isChecked())
|
|
|
|
def on_level_changed(self, checked):
|
|
|
|
target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
|
|
|
|
# if 'Roland' in target_obj.pp_excellon_name or 'Roland' in target_obj.pp_geometry_name or 'hpgl' in \
|
|
# target_obj.pp_geometry_name:
|
|
# # TODO DO NOT AUTOLEVELL
|
|
# pass
|
|
|
|
if not checked:
|
|
self.ui.level.setText('%s' % _('Beginner'))
|
|
self.ui.level.setStyleSheet("""
|
|
QToolButton
|
|
{
|
|
color: green;
|
|
}
|
|
""")
|
|
|
|
self.ui.al_title.hide()
|
|
self.ui.show_al_table.hide()
|
|
self.ui.al_probe_points_table.hide()
|
|
|
|
# Context Menu section
|
|
# self.ui.al_probe_points_table.removeContextMenu()
|
|
else:
|
|
self.ui.level.setText('%s' % _('Advanced'))
|
|
self.ui.level.setStyleSheet("""
|
|
QToolButton
|
|
{
|
|
color: red;
|
|
}
|
|
""")
|
|
|
|
self.ui.al_title.show()
|
|
self.ui.show_al_table.show()
|
|
if self.ui.show_al_table.get_value():
|
|
self.ui.al_probe_points_table.show()
|
|
|
|
# Context Menu section
|
|
# self.ui.al_probe_points_table.setupContextMenu()
|
|
|
|
def build_tool_ui(self):
|
|
self.ui_disconnect()
|
|
|
|
self.build_al_table()
|
|
|
|
self.ui_connect()
|
|
|
|
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.ItemFlag.ItemIsEnabled)
|
|
coords_item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
height_item.setFlags(QtCore.Qt.ItemFlag.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.ResizeMode.Fixed)
|
|
h_header.resizeSection(0, 20)
|
|
h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
|
h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.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 to_form(self, storage=None):
|
|
if storage is None:
|
|
storage = self.app.options
|
|
|
|
for k in self.form_fields:
|
|
for option in storage:
|
|
if option.startswith('tools_al_'):
|
|
if k == option:
|
|
try:
|
|
self.form_fields[k].set_value(storage[option])
|
|
except Exception:
|
|
# it may fail for form fields found in the tools tables if there are no rows
|
|
pass
|
|
|
|
def on_add_al_probepoints(self):
|
|
# create the solid_geo
|
|
|
|
loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
|
|
if loaded_obj is None:
|
|
self.app.log.error("ToolLevelling.on_add_al_probepoints() -> No object loaded.")
|
|
return 'fail'
|
|
|
|
try:
|
|
self.solid_geo = unary_union([geo['geom'] for geo in loaded_obj.gcode_parsed if geo['kind'][0] == 'C'])
|
|
except TypeError:
|
|
return 'fail'
|
|
|
|
# reset al table
|
|
self.ui.al_probe_points_table.setRowCount(0)
|
|
|
|
# reset the al dict
|
|
self.al_voronoi_geo_storage.clear()
|
|
|
|
if self.ui.al_mode_radio.get_value() == 'grid':
|
|
self.on_add_grid_points()
|
|
else:
|
|
self.on_add_manual_points()
|
|
|
|
def check_point_over_excellon(self, pol: Polygon, check: bool) -> MultiPolygon:
|
|
if not check:
|
|
return MultiPolygon()
|
|
|
|
fused_geometries = [
|
|
exc_geo
|
|
for obj_in_collection in self.app.collection.get_list()
|
|
if obj_in_collection.kind == 'excellon' and obj_in_collection.obj_options['plot']
|
|
for exc_geo in MultiPolygon(obj_in_collection.solid_geometry).geoms
|
|
if isinstance(exc_geo, Polygon) and pol.intersects(exc_geo)
|
|
]
|
|
return unary_union(fused_geometries)
|
|
|
|
def on_add_grid_points(self):
|
|
check_overlap = self.ui.avoid_exc_holes_cb.get_value()
|
|
avoid_step = self.ui.avoid_exc_holes_size_entry.get_value()
|
|
radius = self.ui.probe_tip_dia_entry.get_value() / 2
|
|
|
|
xmin, ymin, xmax, ymax = self.solid_geo.bounds
|
|
|
|
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)
|
|
)
|
|
|
|
point_buffered = Point(formatted_point).buffer(radius)
|
|
if self.check_point_over_excellon(pol=point_buffered, check=check_overlap).is_empty:
|
|
# do not add the point if is already added
|
|
if formatted_point not in points:
|
|
points.append(formatted_point)
|
|
new_x += dx
|
|
continue
|
|
|
|
box_poly: Polygon = box(
|
|
new_x - dx if (new_x - dx) > xmin else xmin,
|
|
new_y - dy if (new_y - dy) > ymin else ymin,
|
|
new_x + dx if (new_x + dx) < xmax else xmax,
|
|
new_y + dy if (new_y + dy) < ymax else ymax
|
|
)
|
|
|
|
increments = [
|
|
(avoid_step, 0),
|
|
(avoid_step * -1, 0),
|
|
(0, avoid_step),
|
|
(0, avoid_step * -1)
|
|
]
|
|
|
|
for increment in increments:
|
|
break_for_loop = False
|
|
while True:
|
|
# check if the point is within the box
|
|
formatted_point = (
|
|
formatted_point[0] + increment[0],
|
|
formatted_point[1] + increment[1]
|
|
)
|
|
point_buffered = Point(formatted_point).buffer(radius)
|
|
|
|
if not box_poly.contains(point_buffered):
|
|
break
|
|
|
|
# check if the point overlaps an excellon hole
|
|
if self.check_point_over_excellon(pol=point_buffered, check=check_overlap).is_empty:
|
|
# do not add the point if it is already added
|
|
if formatted_point not in points:
|
|
points.append(formatted_point)
|
|
break_for_loop = True
|
|
break
|
|
if break_for_loop:
|
|
break
|
|
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)
|
|
|
|
def on_add_manual_points(self):
|
|
xmin, ymin, xmax, ymax = self.solid_geo.bounds
|
|
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 = self.ui.probe_tip_dia_entry.get_value() / 2
|
|
fprobe_pt_buff = f_probe_pt.buffer(radius)
|
|
|
|
self.app.inform.emit(_("Click on canvas to add a Probe Point..."))
|
|
self.app.options['global_selection_shape'] = False
|
|
|
|
if self.app.use_3d_engine:
|
|
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.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
|
|
|
|
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):
|
|
self.app.log.debug("ToolLevelling.show_probing_geo() -> %s" % ('cleared' if state is False else 'displayed'))
|
|
if reset:
|
|
self.probing_shapes.clear(update=True)
|
|
|
|
points_geo = []
|
|
poly_geo = []
|
|
|
|
al_method = self.ui.al_method_radio.get_value()
|
|
radius = self.ui.probe_tip_dia_entry.get_value() / 2
|
|
|
|
# voronoi diagram
|
|
if al_method == 'v':
|
|
# create the geometry
|
|
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':
|
|
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.use_3d_engine:
|
|
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.use_3d_engine:
|
|
# 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:
|
|
k = self.add_probing_shape(shape=sh, color=edge_color, face_color=random_color(),
|
|
visible=True)
|
|
else:
|
|
k = 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) as e:
|
|
self.app.log.error("ToolLevelling.plot_probing_geo() -> %s" % str(e))
|
|
self.probing_shapes.clear(update=True)
|
|
except Exception as e:
|
|
self.app.log.error("CNCJobObject.plot_probing_geo() --> %s" % str(e))
|
|
else:
|
|
self.probing_shapes.clear(update=True)
|
|
|
|
def add_probing_shape(self, **kwargs):
|
|
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:
|
|
self.app.log.error("CNCJobObject.generate_voronoi_geometry() --> %s" % str(e))
|
|
for pt_index in range(len(pts)):
|
|
new_pts[pt_index] = 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.geoms:
|
|
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_voronoi_geometry_2(self, pts):
|
|
# env = self.solid_geo.envelope
|
|
# fact = 1 if self.units == 'MM' else 0.039
|
|
# env = env.buffer(fact)
|
|
# env_poly = voronoi_poly(tuple(env.exterior.coords))
|
|
#
|
|
# new_pts = [[pt.x, pt.y] for pt in pts]
|
|
# print(new_pts)
|
|
# print(env_poly)
|
|
#
|
|
# # Initialize the algorithm
|
|
# v = Voronoi(env_poly)
|
|
#
|
|
# # calculate the Voronoi diagram
|
|
# try:
|
|
# v.create_diagram(new_pts)
|
|
# except AttributeError as e:
|
|
# self.app.log.error("CNCJobObject.generate_voronoi_geometry_2() --> %s" % str(e))
|
|
# new_pts_2 = []
|
|
# for pt_index in range(len(new_pts)):
|
|
# new_pts_2.append([
|
|
# new_pts[pt_index][0] + random.random() * 1e-03,
|
|
# new_pts[pt_index][1] + random.random() * 1e-03
|
|
# ])
|
|
#
|
|
# try:
|
|
# v.create_diagram(new_pts_2)
|
|
# except Exception:
|
|
# print("Didn't work.")
|
|
# return
|
|
#
|
|
# new_voronoi = []
|
|
# for p in v.sites:
|
|
# # p_coords = [(coord.x, coord.y) for coord in p.get_coordinates()]
|
|
# p_coords = [(p.x, p.y)]
|
|
# new_pol = Polygon(p_coords)
|
|
# new_voronoi.append(new_pol)
|
|
#
|
|
# new_voronoi = MultiPolygon(new_voronoi)
|
|
#
|
|
# # 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) or \
|
|
# self.al_voronoi_geo_storage[pt_key]['point'].intersects(poly):
|
|
# self.al_voronoi_geo_storage[pt_key]['geo'] = poly
|
|
|
|
def generate_bilinear_geometry(self, pts):
|
|
self.al_bilinear_geo_storage = pts
|
|
|
|
def on_mouse_move(self, event):
|
|
"""
|
|
Callback for the mouse motion event over the plot.
|
|
|
|
:param event: Contains information about the event.
|
|
:return: None
|
|
"""
|
|
|
|
if self.app.use_3d_engine:
|
|
self.mouse_is_dragging = event.is_dragging
|
|
else:
|
|
self.mouse_is_dragging = self.app.plotcanvas.is_dragging
|
|
|
|
# So it can receive key presses but not when the Tcl Shell is active
|
|
if not self.app.ui.shell_dock.isVisible():
|
|
if not self.app.plotcanvas.native.hasFocus():
|
|
self.app.plotcanvas.native.setFocus()
|
|
|
|
# To be called after clicking on the plot.
|
|
def on_mouse_click_release(self, event):
|
|
if self.app.use_3d_engine:
|
|
event_pos = event.pos
|
|
right_button = 2
|
|
else:
|
|
event_pos = (event.xdata, event.ydata)
|
|
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:
|
|
check_for_exc_hole = self.ui.avoid_exc_holes_cb.get_value()
|
|
|
|
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])
|
|
|
|
# do not add the point if is already added
|
|
old_points_coords = [(pt['point'].x, pt['point'].y) for pt in self.al_voronoi_geo_storage.values()]
|
|
if (snapped_pos[0], snapped_pos[1]) in old_points_coords:
|
|
return
|
|
|
|
# Clicked Point
|
|
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
|
|
|
|
# check if chosen point is within an Excellon drill hole geometry
|
|
if check_for_exc_hole is True:
|
|
for obj_in_collection in self.app.collection.get_list():
|
|
if obj_in_collection.kind == 'excellon' and obj_in_collection.obj_options['plot'] is True:
|
|
exc_solid_geometry = MultiPolygon(obj_in_collection.solid_geometry)
|
|
for exc_geo in exc_solid_geometry.geoms:
|
|
if probe_pt.within(exc_geo):
|
|
self.app.inform.emit(_("Point on an Excellon drill hole. 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 = self.ui.probe_tip_dia_entry.get_value() / 2
|
|
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.use_3d_engine:
|
|
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)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
|
|
else:
|
|
self.app.plotcanvas.graph_event_disconnect(self.kp)
|
|
self.app.plotcanvas.graph_event_disconnect(self.mr)
|
|
self.app.plotcanvas.graph_event_disconnect(self.mm)
|
|
|
|
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.options['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 isinstance(event, int):
|
|
key = event
|
|
# events from the GUI are of type QKeyEvent
|
|
elif isinstance(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.KeyboardModifier.ControlModifier
|
|
pass
|
|
elif mod.lower() == 'alt':
|
|
# modifiers = QtCore.Qt.KeyboardModifier.AltModifier
|
|
pass
|
|
elif mod.lower() == 'shift':
|
|
# modifiers = QtCore.Qt.KeyboardModifier.
|
|
pass
|
|
else:
|
|
# modifiers = QtCore.Qt.KeyboardModifier.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.Key_Escape or key == 'Escape':
|
|
if self.mouse_events_connected is True:
|
|
self.mouse_events_connected = False
|
|
if self.app.use_3d_engine:
|
|
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)
|
|
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
|
|
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)
|
|
# restore selection
|
|
self.app.options['global_selection_shape'] = self.old_selection_state
|
|
|
|
# Grid toggle
|
|
if key == QtCore.Qt.Key.Key_G or key == 'G':
|
|
self.app.ui.grid_snap_btn.trigger()
|
|
|
|
# Jump to coords
|
|
if key == QtCore.Qt.Key.Key_J or key == 'J':
|
|
self.app.on_jump_to()
|
|
|
|
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_method_radio.set_value('v')
|
|
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.avoid_exc_holes_cb.setDisabled(False)
|
|
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.options['tools_al_method'])
|
|
# self.ui.avoid_exc_holes_cb.setDisabled(True)
|
|
|
|
def on_avoid_exc_holes(self, state):
|
|
self.ui.avoid_exc_holes_size_label.show() if state else self.ui.avoid_exc_holes_size_label.hide()
|
|
self.ui.avoid_exc_holes_size_entry.show() if state else self.ui.avoid_exc_holes_size_entry.hide()
|
|
|
|
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):
|
|
self.on_controller_change_alter_ui()
|
|
|
|
# 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 generated (it is different for GRBL on how it gets it's Probing GCode
|
|
target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
|
|
if (not self.probing_gcode_text or self.probing_gcode_text == '') and target_obj is not None:
|
|
# 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)
|
|
|
|
def on_controller_change_alter_ui(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()
|
|
|
|
@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('%s...' % _("Sending")):
|
|
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:
|
|
self.app.log.error("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:
|
|
self.app.log.error("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.options["tools_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
|
|
"""
|
|
|
|
target_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
|
|
|
|
p_gcode = ''
|
|
header = ''
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(dt.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(target_obj.obj_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:
|
|
self.app.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, target_obj.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, target_obj.coords_decimals)),
|
|
str(self.app.dec_format(y, target_obj.coords_decimals))
|
|
)
|
|
# do the probing
|
|
p_gcode += "%s Z%s F%s\n" % (
|
|
probing_command,
|
|
str(self.app.dec_format(pr_depth, target_obj.coords_decimals)),
|
|
str(self.app.dec_format(probe_fr, target_obj.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, target_obj.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.options['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.options['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('%s...' % _("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:
|
|
self.app.log.error('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:
|
|
self.app.log.error("Failed to open height map file: %s" % filename)
|
|
self.app.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('%s...' % _("Sending")):
|
|
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 autolevelling 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.options['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 ui_connect(self):
|
|
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):
|
|
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 reset_fields(self):
|
|
self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
|
|
|
|
|
class LevelUI:
|
|
pluginName = _("Levelling")
|
|
|
|
def __init__(self, layout, app):
|
|
self.app = app
|
|
self.decimals = self.app.decimals
|
|
self.layout = layout
|
|
|
|
self.tools_frame = QtWidgets.QFrame()
|
|
self.tools_frame.setContentsMargins(0, 0, 0, 0)
|
|
self.layout.addWidget(self.tools_frame)
|
|
self.tools_box = QtWidgets.QVBoxLayout()
|
|
self.tools_box.setContentsMargins(0, 0, 0, 0)
|
|
self.tools_frame.setLayout(self.tools_box)
|
|
|
|
self.title_box = QtWidgets.QHBoxLayout()
|
|
self.tools_box.addLayout(self.title_box)
|
|
|
|
# ## Title
|
|
title_label = FCLabel("%s" % self.pluginName, size=16, bold=True)
|
|
title_label.setToolTip(
|
|
_("Generate CNC Code with auto-levelled paths.")
|
|
)
|
|
|
|
self.title_box.addWidget(title_label)
|
|
|
|
# App Level label
|
|
self.level = QtWidgets.QToolButton()
|
|
self.level.setToolTip(
|
|
_(
|
|
"Beginner Mode - many parameters are hidden.\n"
|
|
"Advanced Mode - full control.\n"
|
|
"Permanent change is done in 'Preferences' menu."
|
|
)
|
|
)
|
|
# self.level.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter)
|
|
self.level.setCheckable(True)
|
|
self.title_box.addWidget(self.level)
|
|
|
|
self.obj_combo_label = FCLabel('%s' % _("Source Object"), color='darkorange', bold=True)
|
|
self.obj_combo_label.setToolTip(
|
|
_("CNCJob source object to be levelled.")
|
|
)
|
|
|
|
self.tools_box.addWidget(self.obj_combo_label)
|
|
|
|
# #############################################################################################################
|
|
# ################################ The object to be Auto-levelled ##############################################
|
|
# #############################################################################################################
|
|
self.object_combo = FCComboBox()
|
|
self.object_combo.setModel(self.app.collection)
|
|
self.object_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex()))
|
|
# self.object_combo.setCurrentIndex(1)
|
|
self.object_combo.is_last = True
|
|
|
|
self.tools_box.addWidget(self.object_combo)
|
|
|
|
# separator_line = QtWidgets.QFrame()
|
|
# separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
|
|
# separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
|
|
# self.tools_box.addWidget(separator_line)
|
|
|
|
# Autolevelling
|
|
self.al_frame = QtWidgets.QFrame()
|
|
self.al_frame.setContentsMargins(0, 0, 0, 0)
|
|
self.tools_box.addWidget(self.al_frame)
|
|
|
|
self.al_box = QtWidgets.QVBoxLayout()
|
|
self.al_box.setContentsMargins(0, 0, 0, 0)
|
|
self.al_frame.setLayout(self.al_box)
|
|
self.al_frame.setDisabled(True)
|
|
|
|
grid0 = GLay(v_spacing=5, h_spacing=3)
|
|
self.al_box.addLayout(grid0)
|
|
|
|
self.al_title = FCLabel('%s' % _("Probe Points Table"), bold=True)
|
|
self.al_title.setToolTip(_("Generate GCode that will obtain the height map"))
|
|
|
|
self.show_al_table = FCCheckBox(_("Show"))
|
|
self.show_al_table.setToolTip(_("Toggle the display of the Probe Points table."))
|
|
self.show_al_table.setChecked(True)
|
|
|
|
hor_lay = QtWidgets.QHBoxLayout()
|
|
hor_lay.addWidget(self.al_title)
|
|
hor_lay.addStretch()
|
|
hor_lay.addWidget(self.show_al_table, alignment=QtCore.Qt.AlignmentFlag.AlignRight)
|
|
|
|
grid0.addLayout(hor_lay, 0, 0, 1, 2)
|
|
|
|
# #############################################################################################################
|
|
# Tool Table Frame
|
|
# #############################################################################################################
|
|
tt_frame = FCFrame()
|
|
self.al_box.addWidget(tt_frame)
|
|
|
|
# Grid Layout
|
|
tool_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1])
|
|
tt_frame.setLayout(tool_grid)
|
|
|
|
# Probe Points table
|
|
self.al_probe_points_table = FCTable()
|
|
self.al_probe_points_table.setColumnCount(3)
|
|
self.al_probe_points_table.setColumnWidth(0, 20)
|
|
self.al_probe_points_table.setHorizontalHeaderLabels(['#', _('X-Y Coordinates'), _('Height')])
|
|
|
|
tool_grid.addWidget(self.al_probe_points_table, 0, 0, 1, 2)
|
|
|
|
# Plot Probe Points
|
|
self.plot_probing_pts_cb = FCCheckBox(_("Plot probing points"))
|
|
self.plot_probing_pts_cb.setToolTip(
|
|
_("Plot the probing points in the table.\n"
|
|
"If a Voronoi method is used then\n"
|
|
"the Voronoi areas are also plotted.")
|
|
)
|
|
tool_grid.addWidget(self.plot_probing_pts_cb, 2, 0, 1, 2)
|
|
|
|
# Avoid Excellon holes
|
|
self.avoid_exc_holes_cb = FCCheckBox(_("Avoid Excellon holes"))
|
|
self.avoid_exc_holes_cb.setToolTip(
|
|
_("When active, the user cannot add probe points over a drill hole.")
|
|
)
|
|
tool_grid.addWidget(self.avoid_exc_holes_cb, 4, 0, 1, 2)
|
|
|
|
# Avoid Excellon holes Size
|
|
self.avoid_exc_holes_size_label = FCLabel('%s:' % _("Avoid Step"))
|
|
self.avoid_exc_holes_size_label.setToolTip(
|
|
_("The incremental size to move to the side, to avoid an Excellon hole.")
|
|
)
|
|
|
|
self.avoid_exc_holes_size_entry = FCDoubleSpinner()
|
|
self.avoid_exc_holes_size_entry.set_precision(self.decimals)
|
|
self.avoid_exc_holes_size_entry.set_range(0.0000, 99999.0000)
|
|
|
|
tool_grid.addWidget(self.avoid_exc_holes_size_label, 6, 0)
|
|
tool_grid.addWidget(self.avoid_exc_holes_size_entry, 6, 1, 1, 1)
|
|
|
|
# #############################################################################################################
|
|
# ############### Probe GCode Generation ######################################################################
|
|
# #############################################################################################################
|
|
self.probe_gc_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True)
|
|
self.probe_gc_label.setToolTip(
|
|
_("Will create a GCode which will be sent to the controller,\n"
|
|
"either through a file or directly, with the intent to get the height map\n"
|
|
"that is to modify the original GCode to level the cutting height.")
|
|
)
|
|
self.al_box.addWidget(self.probe_gc_label)
|
|
|
|
tp_frame = FCFrame()
|
|
self.al_box.addWidget(tp_frame)
|
|
|
|
# Grid Layout
|
|
param_grid = GLay(v_spacing=5, h_spacing=3)
|
|
tp_frame.setLayout(param_grid)
|
|
|
|
# Probe Diameter
|
|
self.probe_tip_dia_label = FCLabel('%s:' % _("Probe Tip Dia"))
|
|
self.probe_tip_dia_label.setToolTip(
|
|
_("The probe tip diameter.")
|
|
)
|
|
self.probe_tip_dia_entry = FCDoubleSpinner()
|
|
self.probe_tip_dia_entry.set_precision(self.decimals)
|
|
self.probe_tip_dia_entry.set_range(0.0000, 10.0000)
|
|
|
|
param_grid.addWidget(self.probe_tip_dia_label, 0, 0)
|
|
param_grid.addWidget(self.probe_tip_dia_entry, 0, 1)
|
|
|
|
# Travel Z Probe
|
|
self.ptravelz_label = FCLabel('%s:' % _("Probe Z travel"))
|
|
self.ptravelz_label.setToolTip(
|
|
_("The safe Z for probe travelling between probe points.")
|
|
)
|
|
self.ptravelz_entry = FCDoubleSpinner()
|
|
self.ptravelz_entry.set_precision(self.decimals)
|
|
self.ptravelz_entry.set_range(0.0000, 10000.0000)
|
|
|
|
param_grid.addWidget(self.ptravelz_label, 2, 0)
|
|
param_grid.addWidget(self.ptravelz_entry, 2, 1)
|
|
|
|
# Probe depth
|
|
self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
|
|
self.pdepth_label.setToolTip(
|
|
_("The maximum depth that the probe is allowed\n"
|
|
"to probe. Negative value, in current units.")
|
|
)
|
|
self.pdepth_entry = FCDoubleSpinner()
|
|
self.pdepth_entry.set_precision(self.decimals)
|
|
self.pdepth_entry.set_range(-910000.0000, 0.0000)
|
|
|
|
param_grid.addWidget(self.pdepth_label, 4, 0)
|
|
param_grid.addWidget(self.pdepth_entry, 4, 1)
|
|
|
|
# Probe feedrate
|
|
self.feedrate_probe_label = FCLabel('%s:' % _("Probe Feedrate"))
|
|
self.feedrate_probe_label.setToolTip(
|
|
_("The feedrate used while the probe is probing.")
|
|
)
|
|
self.feedrate_probe_entry = FCDoubleSpinner()
|
|
self.feedrate_probe_entry.set_precision(self.decimals)
|
|
self.feedrate_probe_entry.set_range(0, 910000.0000)
|
|
|
|
param_grid.addWidget(self.feedrate_probe_label, 6, 0)
|
|
param_grid.addWidget(self.feedrate_probe_entry, 6, 1)
|
|
|
|
separator_line = QtWidgets.QFrame()
|
|
separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
|
|
separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
|
|
param_grid.addWidget(separator_line, 8, 0, 1, 2)
|
|
|
|
# AUTOLEVELL MODE
|
|
al_mode_lbl = FCLabel('%s' % _("Mode"), bold=True)
|
|
al_mode_lbl.setToolTip(_("Choose a mode for height map generation.\n"
|
|
"- Manual: will pick a selection of probe points by clicking on canvas\n"
|
|
"- Grid: will automatically generate a grid of probe points"))
|
|
|
|
self.al_mode_radio = RadioSet(
|
|
[
|
|
{'label': _('Manual'), 'value': 'manual'},
|
|
{'label': _('Grid'), 'value': 'grid'}
|
|
])
|
|
param_grid.addWidget(al_mode_lbl, 10, 0)
|
|
param_grid.addWidget(self.al_mode_radio, 10, 1)
|
|
|
|
# AUTOLEVELL METHOD
|
|
self.al_method_lbl = FCLabel('%s:' % _("Method"))
|
|
self.al_method_lbl.setToolTip(_("Choose a method for approximation of heights from autolevelling data.\n"
|
|
"- Voronoi: will generate a Voronoi diagram\n"
|
|
"- Bilinear: will use bilinear interpolation. Usable only for grid mode."))
|
|
|
|
self.al_method_radio = RadioSet(
|
|
[
|
|
{'label': _('Voronoi'), 'value': 'v'},
|
|
{'label': _('Bilinear'), 'value': 'b'}
|
|
])
|
|
self.al_method_lbl.setDisabled(True)
|
|
self.al_method_radio.setDisabled(True)
|
|
self.al_method_radio.set_value('v')
|
|
|
|
param_grid.addWidget(self.al_method_lbl, 12, 0)
|
|
param_grid.addWidget(self.al_method_radio, 12, 1)
|
|
|
|
# ## Columns
|
|
self.al_columns_entry = FCSpinner()
|
|
self.al_columns_entry.setMinimum(2)
|
|
|
|
self.al_columns_label = FCLabel('%s:' % _("Columns"))
|
|
self.al_columns_label.setToolTip(
|
|
_("The number of grid columns.")
|
|
)
|
|
param_grid.addWidget(self.al_columns_label, 14, 0)
|
|
param_grid.addWidget(self.al_columns_entry, 14, 1)
|
|
|
|
# ## Rows
|
|
self.al_rows_entry = FCSpinner()
|
|
self.al_rows_entry.setMinimum(2)
|
|
|
|
self.al_rows_label = FCLabel('%s:' % _("Rows"))
|
|
self.al_rows_label.setToolTip(
|
|
_("The number of grid rows.")
|
|
)
|
|
param_grid.addWidget(self.al_rows_label, 16, 0)
|
|
param_grid.addWidget(self.al_rows_entry, 16, 1)
|
|
|
|
self.al_add_button = FCButton(_("Add Probe Points"))
|
|
self.al_box.addWidget(self.al_add_button)
|
|
|
|
# #############################################################################################################
|
|
# Controller Frame
|
|
# #############################################################################################################
|
|
self.al_controller_label = FCLabel('%s' % _("Controller"), color='red', bold=True)
|
|
self.al_controller_label.setToolTip(
|
|
_("The kind of controller for which to generate\n"
|
|
"height map gcode.")
|
|
)
|
|
self.al_box.addWidget(self.al_controller_label)
|
|
|
|
self.c_frame = FCFrame()
|
|
self.al_box.addWidget(self.c_frame)
|
|
|
|
ctrl_grid = GLay(v_spacing=5, h_spacing=3)
|
|
self.c_frame.setLayout(ctrl_grid)
|
|
|
|
self.al_controller_combo = FCComboBox()
|
|
self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"])
|
|
ctrl_grid.addWidget(self.al_controller_combo, 0, 0, 1, 2)
|
|
|
|
# #############################################################################################################
|
|
# ########################## GRBL frame #######################################################################
|
|
# #############################################################################################################
|
|
self.grbl_frame = QtWidgets.QFrame()
|
|
self.grbl_frame.setContentsMargins(0, 0, 0, 0)
|
|
ctrl_grid.addWidget(self.grbl_frame, 2, 0, 1, 2)
|
|
|
|
self.grbl_box = QtWidgets.QVBoxLayout()
|
|
self.grbl_box.setContentsMargins(0, 0, 0, 0)
|
|
self.grbl_frame.setLayout(self.grbl_box)
|
|
|
|
# #############################################################################################################
|
|
# ########################## GRBL TOOLBAR #####################################################################
|
|
# #############################################################################################################
|
|
self.al_toolbar = FCDetachableTab(protect=True)
|
|
self.al_toolbar.setTabsClosable(False)
|
|
self.al_toolbar.useOldIndex(True)
|
|
self.al_toolbar.set_detachable(val=False)
|
|
self.grbl_box.addWidget(self.al_toolbar)
|
|
|
|
# GRBL Connect TAB
|
|
self.gr_conn_tab = QtWidgets.QWidget()
|
|
self.gr_conn_tab.setObjectName("connect_tab")
|
|
self.gr_conn_tab_layout = QtWidgets.QVBoxLayout(self.gr_conn_tab)
|
|
self.gr_conn_tab_layout.setContentsMargins(2, 2, 2, 2)
|
|
# self.gr_conn_scroll_area = VerticalScrollArea()
|
|
# self.gr_conn_tab_layout.addWidget(self.gr_conn_scroll_area)
|
|
self.al_toolbar.addTab(self.gr_conn_tab, _("Connect"))
|
|
|
|
# GRBL Control TAB
|
|
self.gr_ctrl_tab = QtWidgets.QWidget()
|
|
self.gr_ctrl_tab.setObjectName("connect_tab")
|
|
self.gr_ctrl_tab_layout = QtWidgets.QVBoxLayout(self.gr_ctrl_tab)
|
|
self.gr_ctrl_tab_layout.setContentsMargins(2, 2, 2, 2)
|
|
|
|
# self.gr_ctrl_scroll_area = VerticalScrollArea()
|
|
# self.gr_ctrl_tab_layout.addWidget(self.gr_ctrl_scroll_area)
|
|
self.al_toolbar.addTab(self.gr_ctrl_tab, _("Control"))
|
|
|
|
# GRBL Sender TAB
|
|
self.gr_send_tab = QtWidgets.QWidget()
|
|
self.gr_send_tab.setObjectName("connect_tab")
|
|
self.gr_send_tab_layout = QtWidgets.QVBoxLayout(self.gr_send_tab)
|
|
self.gr_send_tab_layout.setContentsMargins(2, 2, 2, 2)
|
|
|
|
# self.gr_send_scroll_area = VerticalScrollArea()
|
|
# self.gr_send_tab_layout.addWidget(self.gr_send_scroll_area)
|
|
self.al_toolbar.addTab(self.gr_send_tab, _("Sender"))
|
|
|
|
for idx in range(self.al_toolbar.count()):
|
|
if self.al_toolbar.tabText(idx) == _("Connect"):
|
|
self.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
|
|
if self.al_toolbar.tabText(idx) == _("Control"):
|
|
self.al_toolbar.tabBar.setTabEnabled(idx, False)
|
|
if self.al_toolbar.tabText(idx) == _("Sender"):
|
|
self.al_toolbar.tabBar.setTabEnabled(idx, False)
|
|
# #############################################################################################################
|
|
|
|
# #############################################################################################################
|
|
# GRBL CONNECT
|
|
# #############################################################################################################
|
|
self.connect_frame = FCFrame()
|
|
self.gr_conn_tab_layout.addWidget(self.connect_frame)
|
|
|
|
grbl_conn_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0])
|
|
self.connect_frame.setLayout(grbl_conn_grid)
|
|
|
|
# COM list
|
|
self.com_list_label = FCLabel('%s:' % _("COM list"))
|
|
self.com_list_label.setToolTip(
|
|
_("Lists the available serial ports.")
|
|
)
|
|
|
|
self.com_list_combo = FCComboBox()
|
|
self.com_search_button = FCButton(_("Search"))
|
|
self.com_search_button.setToolTip(
|
|
_("Search for the available serial ports.")
|
|
)
|
|
grbl_conn_grid.addWidget(self.com_list_label, 2, 0)
|
|
grbl_conn_grid.addWidget(self.com_list_combo, 2, 1)
|
|
grbl_conn_grid.addWidget(self.com_search_button, 2, 2)
|
|
|
|
# BAUDRATES list
|
|
self.baudrates_list_label = FCLabel('%s:' % _("Baud rates"))
|
|
self.baudrates_list_label.setToolTip(
|
|
_("Lists the available serial ports.")
|
|
)
|
|
|
|
self.baudrates_list_combo = FCComboBox()
|
|
cb_model = QtCore.QStringListModel()
|
|
self.baudrates_list_combo.setModel(cb_model)
|
|
self.baudrates_list_combo.addItems(
|
|
['9600', '19200', '38400', '57600', '115200', '230400', '460800', '500000', '576000', '921600', '1000000',
|
|
'1152000', '1500000', '2000000'])
|
|
self.baudrates_list_combo.setCurrentText('115200')
|
|
|
|
grbl_conn_grid.addWidget(self.baudrates_list_label, 4, 0)
|
|
grbl_conn_grid.addWidget(self.baudrates_list_combo, 4, 1)
|
|
|
|
# New baudrate
|
|
self.new_bd_label = FCLabel('%s:' % _("New"))
|
|
self.new_bd_label.setToolTip(
|
|
_("New, custom baudrate.")
|
|
)
|
|
|
|
self.new_baudrate_entry = FCSpinner()
|
|
self.new_baudrate_entry.set_range(40, 9999999)
|
|
|
|
self.add_bd_button = FCButton(_("Add"))
|
|
self.add_bd_button.setToolTip(
|
|
_("Add the specified custom baudrate to the list.")
|
|
)
|
|
grbl_conn_grid.addWidget(self.new_bd_label, 6, 0)
|
|
grbl_conn_grid.addWidget(self.new_baudrate_entry, 6, 1)
|
|
grbl_conn_grid.addWidget(self.add_bd_button, 6, 2)
|
|
|
|
self.del_bd_button = FCButton(_("Delete selected baudrate"))
|
|
grbl_conn_grid.addWidget(self.del_bd_button, 8, 0, 1, 3)
|
|
|
|
ctrl_h_lay = QtWidgets.QHBoxLayout()
|
|
self.controller_reset_button = FCButton(_("Reset"))
|
|
self.controller_reset_button.setToolTip(
|
|
_("Software reset of the controller.")
|
|
)
|
|
self.controller_reset_button.setDisabled(True)
|
|
ctrl_h_lay.addWidget(self.controller_reset_button)
|
|
|
|
self.com_connect_button = FCButton()
|
|
self.com_connect_button.setText(_("Disconnected"))
|
|
self.com_connect_button.setToolTip(
|
|
_("Connect to the selected port with the selected baud rate.")
|
|
)
|
|
self.com_connect_button.setStyleSheet("QPushButton {background-color: red;}")
|
|
ctrl_h_lay.addWidget(self.com_connect_button)
|
|
|
|
grbl_conn_grid.addWidget(FCLabel(""), 9, 0, 1, 3)
|
|
grbl_conn_grid.setRowStretch(9, 1)
|
|
grbl_conn_grid.addLayout(ctrl_h_lay, 10, 0, 1, 3)
|
|
|
|
# #############################################################################################################
|
|
# GRBL CONTROL
|
|
# #############################################################################################################
|
|
self.ctrl_grbl_frame = FCFrame()
|
|
self.gr_ctrl_tab_layout.addWidget(self.ctrl_grbl_frame)
|
|
grbl_ctrl_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0])
|
|
self.ctrl_grbl_frame.setLayout(grbl_ctrl_grid)
|
|
|
|
self.ctrl_grbl_frame2 = FCFrame()
|
|
self.gr_ctrl_tab_layout.addWidget(self.ctrl_grbl_frame2)
|
|
grbl_ctrl2_grid = GLay(v_spacing=5, h_spacing=3)
|
|
self.ctrl_grbl_frame2.setLayout(grbl_ctrl2_grid)
|
|
|
|
self.gr_ctrl_tab_layout.addStretch(1)
|
|
|
|
jog_title_label = FCLabel(_("Jog"), bold=True)
|
|
|
|
zero_title_label = FCLabel(_("Zero Axes"), bold=True)
|
|
# zero_title_label.setStyleSheet("""
|
|
# FCLabel
|
|
# {
|
|
# font-weight: bold;
|
|
# }
|
|
# """)
|
|
|
|
grbl_ctrl_grid.addWidget(jog_title_label, 0, 0)
|
|
grbl_ctrl_grid.addWidget(zero_title_label, 0, 2)
|
|
|
|
self.jog_wdg = FCJog(self.app)
|
|
self.jog_wdg.setStyleSheet("""
|
|
FCJog
|
|
{
|
|
border: 1px solid lightgray;
|
|
border-radius: 5px;
|
|
}
|
|
""")
|
|
|
|
self.zero_axs_wdg = FCZeroAxes(self.app)
|
|
self.zero_axs_wdg.setStyleSheet("""
|
|
FCZeroAxes
|
|
{
|
|
border: 1px solid lightgray;
|
|
border-radius: 5px
|
|
}
|
|
""")
|
|
grbl_ctrl_grid.addWidget(self.jog_wdg, 2, 0)
|
|
grbl_ctrl_grid.addWidget(self.zero_axs_wdg, 2, 2)
|
|
|
|
self.pause_resume_button = RotatedToolButton()
|
|
self.pause_resume_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum,
|
|
QtWidgets.QSizePolicy.Policy.Expanding)
|
|
self.pause_resume_button.setText(_("Pause/Resume"))
|
|
self.pause_resume_button.setCheckable(True)
|
|
self.pause_resume_button.setStyleSheet("""
|
|
RotatedToolButton:checked
|
|
{
|
|
background-color: red;
|
|
color: white;
|
|
border: none;
|
|
}
|
|
""")
|
|
|
|
pause_frame = QtWidgets.QFrame()
|
|
pause_frame.setContentsMargins(0, 0, 0, 0)
|
|
pause_frame.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored, QtWidgets.QSizePolicy.Policy.Expanding)
|
|
pause_h_lay = QtWidgets.QHBoxLayout()
|
|
pause_h_lay.setContentsMargins(0, 0, 0, 0)
|
|
|
|
pause_h_lay.addWidget(self.pause_resume_button)
|
|
pause_frame.setLayout(pause_h_lay)
|
|
grbl_ctrl_grid.addWidget(pause_frame, 2, 1)
|
|
|
|
# JOG Step
|
|
self.jog_step_label = FCLabel('%s:' % _("Step"))
|
|
self.jog_step_label.setToolTip(
|
|
_("Each jog action will move the axes with this value.")
|
|
)
|
|
|
|
self.jog_step_entry = FCSliderWithDoubleSpinner()
|
|
self.jog_step_entry.set_precision(self.decimals)
|
|
self.jog_step_entry.setSingleStep(0.1)
|
|
self.jog_step_entry.set_range(0, 500)
|
|
|
|
grbl_ctrl2_grid.addWidget(self.jog_step_label, 0, 0)
|
|
grbl_ctrl2_grid.addWidget(self.jog_step_entry, 0, 1)
|
|
|
|
# JOG Feedrate
|
|
self.jog_fr_label = FCLabel('%s:' % _("Feedrate"))
|
|
self.jog_fr_label.setToolTip(
|
|
_("Feedrate when jogging.")
|
|
)
|
|
|
|
self.jog_fr_entry = FCSliderWithDoubleSpinner()
|
|
self.jog_fr_entry.set_precision(self.decimals)
|
|
self.jog_fr_entry.setSingleStep(10)
|
|
self.jog_fr_entry.set_range(0, 10000)
|
|
|
|
grbl_ctrl2_grid.addWidget(self.jog_fr_label, 1, 0)
|
|
grbl_ctrl2_grid.addWidget(self.jog_fr_entry, 1, 1)
|
|
|
|
# #############################################################################################################
|
|
# GRBL SENDER
|
|
# #############################################################################################################
|
|
self.sender_frame = FCFrame()
|
|
self.gr_send_tab_layout.addWidget(self.sender_frame)
|
|
|
|
grbl_send_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[1, 0])
|
|
self.sender_frame.setLayout(grbl_send_grid)
|
|
|
|
# Send CUSTOM COMMAND
|
|
self.grbl_command_label = FCLabel('%s:' % _("Send Command"))
|
|
self.grbl_command_label.setToolTip(
|
|
_("Send a custom command to GRBL.")
|
|
)
|
|
grbl_send_grid.addWidget(self.grbl_command_label, 2, 0, 1, 2)
|
|
|
|
self.grbl_command_entry = FCEntry()
|
|
self.grbl_command_entry.setPlaceholderText(_("Type GRBL command ..."))
|
|
|
|
self.grbl_send_button = QtWidgets.QToolButton()
|
|
self.grbl_send_button.setText(_("Send"))
|
|
self.grbl_send_button.setToolTip(
|
|
_("Send a custom command to GRBL.")
|
|
)
|
|
grbl_send_grid.addWidget(self.grbl_command_entry, 4, 0)
|
|
grbl_send_grid.addWidget(self.grbl_send_button, 4, 1)
|
|
|
|
# Get Parameter
|
|
self.grbl_get_param_label = FCLabel('%s:' % _("Get Config parameter"))
|
|
self.grbl_get_param_label.setToolTip(
|
|
_("A GRBL configuration parameter.")
|
|
)
|
|
grbl_send_grid.addWidget(self.grbl_get_param_label, 6, 0, 1, 2)
|
|
|
|
self.grbl_parameter_entry = FCEntry()
|
|
self.grbl_parameter_entry.setPlaceholderText(_("Type GRBL parameter ..."))
|
|
|
|
self.grbl_get_param_button = QtWidgets.QToolButton()
|
|
self.grbl_get_param_button.setText(_("Get"))
|
|
self.grbl_get_param_button.setToolTip(
|
|
_("Get the value of a specified GRBL parameter.")
|
|
)
|
|
grbl_send_grid.addWidget(self.grbl_parameter_entry, 8, 0)
|
|
grbl_send_grid.addWidget(self.grbl_get_param_button, 8, 1)
|
|
|
|
grbl_send_grid.setRowStretch(9, 1)
|
|
|
|
# GET Report
|
|
self.grbl_report_button = FCButton(_("Get Report"))
|
|
self.grbl_report_button.setToolTip(
|
|
_("Print in shell the GRBL report.")
|
|
)
|
|
grbl_send_grid.addWidget(self.grbl_report_button, 10, 0, 1, 2)
|
|
|
|
hm_lay = QtWidgets.QHBoxLayout()
|
|
# GET HEIGHT MAP
|
|
self.grbl_get_heightmap_button = FCButton(_("Apply AutoLevelling"))
|
|
self.grbl_get_heightmap_button.setToolTip(
|
|
_("Will send the probing GCode to the GRBL controller,\n"
|
|
"wait for the Z probing data and then apply this data\n"
|
|
"over the original GCode therefore doing autolevelling.")
|
|
)
|
|
hm_lay.addWidget(self.grbl_get_heightmap_button, stretch=1)
|
|
|
|
self.grbl_save_height_map_button = QtWidgets.QToolButton()
|
|
self.grbl_save_height_map_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
|
|
self.grbl_save_height_map_button.setToolTip(
|
|
_("Will save the GRBL height map.")
|
|
)
|
|
hm_lay.addWidget(self.grbl_save_height_map_button, stretch=0, alignment=Qt.AlignmentFlag.AlignRight)
|
|
|
|
grbl_send_grid.addLayout(hm_lay, 12, 0, 1, 2)
|
|
|
|
self.grbl_frame.hide()
|
|
# #############################################################################################################
|
|
|
|
height_lay = QtWidgets.QHBoxLayout()
|
|
self.h_gcode_button = FCButton(_("Save Probing GCode"))
|
|
self.h_gcode_button.setToolTip(
|
|
_("Will save the probing GCode.")
|
|
)
|
|
self.h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding,
|
|
QtWidgets.QSizePolicy.Policy.MinimumExpanding)
|
|
|
|
height_lay.addWidget(self.h_gcode_button)
|
|
self.view_h_gcode_button = QtWidgets.QToolButton()
|
|
self.view_h_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
|
|
# self.view_h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored,
|
|
# QtWidgets.QSizePolicy.Policy.Ignored)
|
|
self.view_h_gcode_button.setToolTip(
|
|
_("View/Edit the probing GCode.")
|
|
)
|
|
# height_lay.addStretch()
|
|
height_lay.addWidget(self.view_h_gcode_button)
|
|
|
|
self.al_box.addLayout(height_lay)
|
|
|
|
self.import_heights_button = FCButton(_("Import Height Map"))
|
|
self.import_heights_button.setToolTip(
|
|
_("Import the file that has the Z heights\n"
|
|
"obtained through probing and then apply this data\n"
|
|
"over the original GCode therefore\n"
|
|
"doing autolevelling.")
|
|
)
|
|
self.al_box.addWidget(self.import_heights_button)
|
|
|
|
# self.h_gcode_button.hide()
|
|
# self.import_heights_button.hide()
|
|
|
|
# separator_line = QtWidgets.QFrame()
|
|
# separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
|
|
# separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
|
|
# grid0.addWidget(separator_line, 35, 0, 1, 2)
|
|
|
|
self.tools_box.addStretch(1)
|
|
|
|
# ## Reset Tool
|
|
self.reset_button = FCButton(_("Reset Tool"), bold=True)
|
|
self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
|
|
self.reset_button.setToolTip(
|
|
_("Will reset the tool parameters.")
|
|
)
|
|
self.tools_box.addWidget(self.reset_button)
|
|
# ############################ FINISHED GUI ###################################
|
|
# #############################################################################
|
|
|
|
GLay.set_common_column_size([tool_grid, param_grid], 0)
|
|
|
|
self.plot_probing_pts_cb.stateChanged.connect(self.on_plot_points_changed)
|
|
self.avoid_exc_holes_cb.stateChanged.connect(self.on_avoid_exc_holes_changed)
|
|
|
|
def confirmation_message(self, accepted, minval, maxval):
|
|
if accepted is False:
|
|
self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
|
|
self.decimals,
|
|
minval,
|
|
self.decimals,
|
|
maxval), False)
|
|
else:
|
|
self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
|
|
|
|
def confirmation_message_int(self, accepted, minval, maxval):
|
|
if accepted is False:
|
|
self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
|
|
(_("Edited value is out of range"), minval, maxval), False)
|
|
else:
|
|
self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
|
|
|
|
def on_plot_points_changed(self, state):
|
|
self.app.options["tools_al_plot_points"] = False if not state else True
|
|
|
|
def on_avoid_exc_holes_changed(self, state):
|
|
self.app.options["tools_al_avoid_exc_holes"] = False if not state else True
|