Files
linuxcncAllWasm/lib/python/qtvcp/widgets/gcode_graphics.py
2026-08-20 06:17:39 +08:00

596 lines
22 KiB
Python

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
#
# Copyright 2016 Chris Morley
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
PyQt5 widget for plotting gcode.
"""
import sys
import os
import gcode
import linuxcnc
from qtpy.QtCore import Property, QTimer, Qt
from qtpy.QtGui import QColor
from qt5_graphics import Lcnc_3dGraphics
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
from qtvcp.core import Status, Info
from qtvcp import logger
# Instantiate the libraries with global reference
# STATUS gives us status messages from linuxcnc
# INFO is INI file details
# LOG is for running code logging
STATUS = Status()
INFO = Info()
LOG = logger.getLogger(__name__)
# Force the log level for this module
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
##############################################
# Container class
##############################################
class GCodeGraphics(Lcnc_3dGraphics, _HalWidgetBase):
def __init__(self, parent=None):
super( GCodeGraphics, self).__init__(parent)
self._overlayColor = QColor(0, 0, 0, 0)
self.colors['overlay_alpha'] = 0.75
self._overlayAlpha = 0.75
self._gridColor = QColor(0, 138, 138)
self._backgroundColor = QColor(0, 0, 191, 150)
self._jogColor = QColor(0, 0, 0, 0)
self._feedColor = QColor(0, 0, 0, 0)
self._rapidColor = QColor(0, 0, 0, 0)
self.use_gradient_background = False
# color1 is the bottom color that blends up to color2
self.gradient_color1 = (0.,0,.5)
self.gradient_color2 = (0,.0, 0)
self.show_overlay = False # no DRO or DRO overlay
self._reload_filename = None
self.show_small_origin = True
self._view_incr = 20
self.inhibit_selection = False
self._block_line_selected = False
# stop response to external STATUS signals
self._disable_STATUS_signals = False
self._block_autoLoad = None
self._block_reLoad = None
self._block_viewChanged = None
self._block_lineSelect = None
self._mouseMode = 0
if INFO.MACHINE_IS_METRIC:
self.mach_units = 'Metric'
else:
self.mach_units = 'Imperial'
def addTimer(self):
self.timer = QTimer()
self.timer.timeout.connect(self.poll)
self.timer.start(INFO.GRAPHICS_CYCLE_TIME)
def _hal_init(self):
self._block_autoLoad = STATUS.connect('file-loaded', self.load_program)
self._block_reLoad = STATUS.connect('reload-display', self.reloadfile)
STATUS.connect('actual-spindle-speed-changed', self.set_spindle_speed)
STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data))
STATUS.connect('metric-mode-changed', lambda w, f: self.set_metric_units(w, f))
self._block_viewChanged = STATUS.connect('graphics-view-changed', lambda w, v, a: self.set_view_signal(v, a))
self._block_lineSelect = STATUS.connect('gcode-line-selected', lambda w, l: self.highlight_graphics(l))
# we do this in this function because the property InhibitControls
# is set before the STATUS (GObject) signal ids can be recorded
if self._disable_STATUS_signals:
self.updateSignals(True)
# If there is a preference file object use it to load the user view position data
if self.PREFS_:
v,z,x,y,lat,lon = self.getCurrentViewSettings()
v = self.PREFS_.getpref(self.HAL_NAME_+'-user-view', v, str, 'SCREEN_CONTROL_LAST_SETTING')
z = self.PREFS_.getpref(self.HAL_NAME_+'-user-zoom', z, float, 'SCREEN_CONTROL_LAST_SETTING')
x = self.PREFS_.getpref(self.HAL_NAME_+'-user-panx', x, float, 'SCREEN_CONTROL_LAST_SETTING')
y = self.PREFS_.getpref(self.HAL_NAME_+'-user-pany', y, float, 'SCREEN_CONTROL_LAST_SETTING')
lat = self.PREFS_.getpref(self.HAL_NAME_+'-user-lat', lat, float, 'SCREEN_CONTROL_LAST_SETTING')
lon = self.PREFS_.getpref(self.HAL_NAME_+'-user-lon', lon, float, 'SCREEN_CONTROL_LAST_SETTING')
self.presetViewSettings(v,z,x,y,lat,lon)
self.set_preferred_view()
# when qtvcp closes, this gets called
def _hal_cleanup(self):
self.timer.stop()
self.logger.stop()
if self.PREFS_:
v,z,x,y,lat,lon = self.getRecordedViewSettings()
LOG.debug('Saving {} data to file.'.format(self.HAL_NAME_))
self.PREFS_.putpref(self.HAL_NAME_+'-user-view', v, str, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-user-zoom', z, float, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-user-panx', x, float, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-user-pany', y, float, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-user-lat', lat, float, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-user-lon', lon, float, 'SCREEN_CONTROL_LAST_SETTING')
self.PREFS_.putpref(self.HAL_NAME_+'-current-view', self.get_current_view(), str, 'SCREEN_CONTROL_LAST_SETTING')
def set_preferred_view(self):
if self.PREFS_:
view = self.PREFS_.getpref(self.HAL_NAME_+'-current-view', 'p', str, 'SCREEN_CONTROL_LAST_SETTING')
# set graphics view variable
self.current_view = view
# tell any other interested widgets that the view changed
STATUS.emit('graphics-view-changed', self.current_view, None)
# external source asked for highlight,
# make sure we block the propagation
def highlight_graphics(self, line):
if self._current_file is None: return
self._block_line_selected = True
self.set_highlight_line(line)
# used to clear highlighting externally
def clear_highlight(self):
self.set_highlight_line(None)
def set_view_signal(self, view, args):
#print(view,args)
v = view.lower()
if v == 'clear':
self.clear_live_plotter()
elif v == 'zoom-in':
self.zoomin()
elif v == 'zoom-out':
self.zoomout()
elif v == 'pan-down':
self.recordMouse(0,0)
self.translateOrRotate(0,self._view_incr)
elif v == 'pan-up':
self.recordMouse(0,0)
self.translateOrRotate(0,-self._view_incr)
elif v == 'pan-right':
self.recordMouse(0,0)
self.translateOrRotate(self._view_incr,0)
elif v == 'pan-left':
self.recordMouse(0,0)
self.translateOrRotate(-self._view_incr,0)
elif v == 'rotate-ccw':
self.recordMouse(0,0)
self.rotateOrTranslate(self._view_incr,0)
elif v == 'rotate-cw':
self.recordMouse(0,0)
self.rotateOrTranslate(-self._view_incr,0)
elif v == 'rotate-up':
self.recordMouse(0,0)
self.rotateOrTranslate(0,self._view_incr)
elif v == 'rotate-down':
self.recordMouse(0,0)
self.rotateOrTranslate(0,-self._view_incr)
elif v == 'overlay-offsets-on':
self.setShowOffsets(True)
elif v == 'overlay-offsets-off':
self.setShowOffsets(False)
elif v == 'overlay-dro-on':
self.setProperty('_dro',True)
elif v == 'overlay-dro-off':
self.setProperty('_dro',False)
elif v == 'dtg-on':
self.setProperty('_dtg',True)
elif v == 'dtg-off':
self.setProperty('_dtg',False)
elif v == 'pan-view':
self.panView(args.get('X'),args.get('Y'))
elif v == 'rotate-view':
self.rotateView(args.get('X'),args.get('Y'))
elif v == 'grid-color':
self.setProperty('grid_color',args.get('COLOR'))
elif v == 'grid-off':
self.grid_size = 0.0
self.update()
elif v == 'grid-size':
self.grid_size = args.get('SIZE')
self.update()
elif v == 'alpha-mode-on':
self.set_alpha_mode(True)
elif v == 'alpha-mode-off':
self.set_alpha_mode(False)
elif v == 'inhibit-selection-on':
self.inhibit_selection = True
elif v == 'inhibit-selection-off':
self.inhibit_selection = False
elif v == 'dimensions-on':
self.show_extents_option = True
self.update()
elif v == 'dimensions-off':
self.show_extents_option = False
self.update()
elif v == 'record-view':
self.recordCurrentViewSettings()
elif v == 'set-recorded-view':
self.setRecordedView()
elif v == 'set-large-dro':
self.set_font(True)
elif v == 'set-small-dro':
self.set_font(False)
elif v == 'scroll-mode':
m = args.get('MODE')
self.setScrollMode(int(m))
elif v in('x', 'y', 'y2', 'z', 'z2', 'p'):
self.set_view(v)
else:
LOG.error('view signal not recognized: {}'.format(v))
def load_program(self, g, fname):
LOG.debug('load the display: {}'.format(fname))
self._reload_filename = fname
result = self.load(fname)
STATUS.emit('graphics-gcode-properties',self.gcode_properties)
# reset the current view to standard calculated zoom and position
self.set_current_view()
return result
def set_metric_units(self, w, state):
self.metric_units = state
self.update()
def set_spindle_speed(self, w, rate):
self.spindle_speed = int(rate)
self.update()
def _tool_info(self, data):
if data.id != -1:
self._tool_dia = self.conversion(data.diameter)
else:
self._tool_dia = 0
# This does the conversion units
# data must always be in machine units
def conversion(self, data):
if self.metric_units :
return INFO.convert_machine_to_metric(data)
else:
return INFO.convert_machine_to_imperial(data)
def set_view(self, value):
view = str(value).lower()
if self.lathe_option:
if view not in ['p', 'y', 'y2']:
return False
elif view not in ['p', 'x', 'y', 'z', 'z2']:
return False
self.current_view = view
if self.initialised:
self.set_current_view()
def reloadfile(self, w):
LOG.debug('reload the display: {}'.format(self._reload_filename))
try:
self.load(self._reload_filename)
self.clear_live_plotter()
STATUS.emit('graphics-gcode-properties',self.gcode_properties)
except:
print('error', self._reload_filename)
pass
def updateSignals(self, state):
if self._block_autoLoad == None:
return
if state:
STATUS.handler_block(self._block_autoLoad)
STATUS.handler_block(self._block_reLoad)
STATUS.handler_block(self._block_viewChanged)
STATUS.handler_block(self._block_lineSelect)
else:
STATUS.handler_unblock(self._block_autoLoad)
STATUS.handler_unblock(self._block_reLoad)
STATUS.handler_unblock(self._block_viewChanged)
STATUS.handler_unblock(self._block_lineSelect)
def updateMouseMode(self, value):
if value == 0:
m = Qt.LeftButton; z = Qt.MiddleButton; r = Qt.RightButton
elif value == 1:
m = Qt.MiddleButton; z = Qt.RightButton; r = Qt.LeftButton
elif value == 2:
m = Qt.MiddleButton; z = Qt.LeftButton; r = Qt.RightButton
elif value == 3:
m = Qt.LeftButton; z = Qt.RightButton; r = Qt.MiddleButton
elif value == 4:
m = Qt.RightButton; z = Qt.LeftButton; r = Qt.MiddleButton
elif value == 5:
m = Qt.RightButton; z = Qt.MiddleButton; r = Qt.LeftButton
elif value == 6:
m = Qt.LeftButton; z = Qt.MiddleButton; r = False
elif value == 7:
m = Qt.MiddleButton; z = Qt.LeftButton; r = False
elif value == 8:
m = Qt.RightButton; z = Qt.LeftButton; r = False
elif value == 9:
m = Qt.LeftButton; z = Qt.RightButton; r = False
elif value == 10:
m = Qt.MiddleButton; z = Qt.RightButton; r = False
elif value == 11:
m = Qt.RightButton; z = Qt.MiddleButton; r = False
else:
return
self._buttonList =[m,z,r]
####################################################
# functions that override qt5_graphics
####################################################
def report_gcode_error(self, result, seq, filename):
error_str = gcode.strerror(result)
errortext = "G-Code error in " + os.path.basename(filename) + "\n" + "Near line " \
+ str(seq) + " of\n" + filename + "\n" + error_str + "\n"
STATUS.emit("graphics-gcode-error", errortext)
# Override qt5_graphics / glcannon.py function so we can emit a GObject signal
# block sending out signal if the highlight request
# came from an external source - we only send it out
# if someone clicked on us
def update_highlight_variable(self, line):
self.highlight_line = line
if self._block_line_selected:
self._block_line_selected = False
return
if line is None:
line = -1
STATUS.emit('graphics-line-selected', line)
def select_fire(self):
if self.inhibit_selection: return
if STATUS.is_auto_running(): return
if not self.select_primed: return
x, y = self.select_primed
self.select_primed = None
self.makeCurrent()
self.select(x, y)
# override user plot -One could add gl commands to plot static objects here
def user_plot(self):
return
def emit_percent(self, f):
super( GCodeGraphics, self).emit_percent(f)
STATUS.emit('graphics-loading-progress',f)
def get_joints_mode(self):
return STATUS.stat.motion_mode == linuxcnc.TRAJ_MODE_FREE
def output_notify_message(self, message):
print("Preview Notified:", message)
mess = {}
mess['TITLE']= "Preview Notified"
mess["SHORTTEXT"]= "See log"
mess['DETAILS']= message
options ={'LOG=True,LEVEL=DEFAULT'}
STATUS.emit('status-message', mess, options)
#########################################################################
# This is how designer can interact with our widget properties.
# property getter/setters
#########################################################################
# VIEW
def setview(self, view):
self.current_view = view
self.set_view(view)
def getview(self):
return self.current_view
def resetview(self):
self.set_view('p')
_view = Property(str, getview, setview, resetview)
# DRO
def setdro(self, state):
self.setEnableDRO(state)
def getdro(self):
return self.getEnableDRO()
_dro = Property(bool, getdro, setdro)
# DTG
def setdtg(self, state):
self.show_dtg = state
self.update()
def getdtg(self):
return self.show_dtg
_dtg = Property(bool, getdtg, setdtg)
# METRIC
def setmetric(self, state):
self.metric_units = state
self.update()
def getmetric(self):
return self.metric_units
_metric = Property(bool, getmetric, setmetric)
# overlay
def setoverlay(self, overlay):
self.show_overlay = overlay
self.update()
def getoverlay(self):
return self.show_overlay
def resetoverlay(self):
self.show_overlay(False)
_overlay = Property(bool, getoverlay, setoverlay, resetoverlay)
# show Offsets
def setShowOffsets(self, state):
self.show_offsets = state
self.update()
def getShowOffsets(self):
return self.show_offsets
_offsets = Property(bool, getShowOffsets, setShowOffsets)
# show small origin
def setShowSmallOrigin(self, state):
self.show_small_origin = state
self.update()
def getShowSmallOrigin(self):
return self.show_small_origin
_small_origin = Property(bool, getShowSmallOrigin, setShowSmallOrigin)
def getGridColor(self):
return self._gridColor
def setGridColor(self, value):
self._gridColor = value
self.colors['grid'] = (value.redF(), value.greenF(), value.blueF())
self.update()
def resetGridColor(self):
self._gridColor = QColor(200, 138, 138)
grid_color = Property(QColor, getGridColor, setGridColor, resetGridColor)
def getOverlayColor(self):
return self._overlayColor
def setOverlayColor(self, value):
self._overlayColor = value
self.colors['overlay_background'] = (value.redF(), value.greenF(), value.blueF())
self.update()
def resetOverlayColor(self):
self._overlayColor = QColor(0, 0, 191, 150)
overlay_color = Property(QColor, getOverlayColor, setOverlayColor, resetOverlayColor)
def getOverlayAlpha(self):
return self._overlayAlpha
def setOverlayAlpha(self, value):
self._overlayAlpha = value
self.colors['overlay_alpha'] = value
self.update()
def resetOverlayAlpha(self):
self._overlayAlpha = 0.75
overlay_alpha = Property(float, getOverlayAlpha, setOverlayAlpha, resetOverlayAlpha)
def getBackgroundColor(self):
return self._backgroundColor
def setBackgroundColor(self, value):
self._backgroundColor = value
#print value.getRgbF()
self.colors['back'] = (value.redF(), value.greenF(), value.blueF())
self.gradient_color1 = (value.redF(), value.greenF(), value.blueF())
self.update()
def resetBackgroundColor(self):
self._backgroundColor = QColor(0, 0, 0, 0)
self.gradient_color1 = QColor(0, 0, 0, 0)
value = QColor(0, 0, 0, 0)
self.gradient_color1 = (value.redF(), value.greenF(), value.blueF())
self.colors['back'] = (value.redF(), value.greenF(), value.blueF())
self.update()
background_color = Property(QColor, getBackgroundColor, setBackgroundColor, resetBackgroundColor)
# use gradient background
def setGradientBackground(self, state):
self.use_gradient_background = state
self.update()
def getGradientBackground(self):
return self.use_gradient_background
_use_gradient_background = Property(bool, getGradientBackground, setGradientBackground)
def getJogColor(self):
return self._jogColor
def setJogColor(self, value):
self._jogColor = value
if value.alpha() == 0:
c = self.get_default_plot_colors()
self.set_plot_colors(jog = c[0])
else:
self.set_plot_colors(jog = (value.red(), value.green(), value.blue(),value.alpha()))
def resetJogColor(self):
self._jogColor = QColor(0, 0, 0, 0)
jog_color = Property(QColor, getJogColor, setJogColor, resetJogColor)
def getFeedColor(self):
return self._feedColor
def setFeedColor(self, value):
self._feedColor = value
if value.alpha() == 0:
c = self.get_default_plot_colors()
self.set_plot_colors(feed = c[2], arc = c[3])
else:
self.set_plot_colors(feed = (value.red(), value.green(), value.blue(),value.alpha()),
arc = (value.red(), value.green(), value.blue(),value.alpha()))
def resetFeedColor(self):
self._feedColor = QColor(0, 0, 0, 0)
Feed_color = Property(QColor, getFeedColor, setFeedColor, resetFeedColor)
def getRapidColor(self):
return self._rapidColor
def setRapidColor(self, value):
self._rapidColor = value
if value.alpha() == 0:
c = self.get_default_plot_colors()
self.set_plot_colors(traverse = c[1])
else:
self.set_plot_colors(traverse = (value.red(), value.green(), value.blue(),value.alpha()))
def resetRapidColor(self):
self._rapidColor = QColor(0, 0, 0, 0)
Rapid_color = Property(QColor, getRapidColor, setRapidColor, resetRapidColor)
# Inhibit external controls
def setInhibitControls(self, state):
self._disable_STATUS_signals = state
self.updateSignals(state)
def getInhibitControls(self):
return self._disable_STATUS_signals
def resetInhibitControls(self):
self._disable_STATUS_signals = False
self.updateSignals(False)
InhibitControls = Property(bool, getInhibitControls, setInhibitControls,resetInhibitControls)
# set Mouse button controls
def setMouseButtonMode(self, value):
self._mouseMode = value
self.updateMouseMode(value)
def getMouseButtonMode(self):
return self._mouseMode
def resetMouseButtonMode(self):
self._mouseMode = 0
self.updateMouseMode(0)
MouseButtonMode = Property(int, getMouseButtonMode, setMouseButtonMode,resetMouseButtonMode)
# set mouse wheel zoom inversion
def setMouseWheelInvertZoom(self, state):
self._invertWheelZoom = state
def getMouseWheelInvertZoom(self):
return self._invertWheelZoom
def resetMouseWheelInvertZoom(self):
self._invertWheelZoom = False
MouseWheelInvertZoom = Property(bool, getMouseWheelInvertZoom, setMouseWheelInvertZoom, resetMouseWheelInvertZoom)
# For testing purposes, include code to allow a widget to be created and shown
# if this file is run.
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = GCodeGraphics()
widget.use_gradient_background = True
widget.enable_dro = True
widget.show()
sys.exit(app.exec())