优化ui,正在优化gcode_viewer
This commit is contained in:
@@ -13,5 +13,18 @@
|
|||||||
"move_max_speed": {
|
"move_max_speed": {
|
||||||
"xy": 3000,
|
"xy": 3000,
|
||||||
"z": 200
|
"z": 200
|
||||||
|
},
|
||||||
|
"home_positions": {
|
||||||
|
"x": 134,
|
||||||
|
"y": 123,
|
||||||
|
"z": 10
|
||||||
|
},
|
||||||
|
"hotend_temp_range": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 300
|
||||||
|
},
|
||||||
|
"bed_temp_range": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +63,17 @@ echo "激活虚拟环境并升级 pip..."
|
|||||||
source "$VENVDIR/bin/activate"
|
source "$VENVDIR/bin/activate"
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
|
|
||||||
|
PYTHON_VERSION=$(ls $VENVDIR/lib/ | grep python3)
|
||||||
|
|
||||||
|
|
||||||
if [ "${install_pyqt_via_pip:-0}" = "1" ]; then
|
if [ "${install_pyqt_via_pip:-0}" = "1" ]; then
|
||||||
echo "在 venv 中通过 pip 安装 PyQt6(回退)..."
|
echo "在 venv 中通过 pip 安装 PyQt6(回退)..."
|
||||||
pip install PyQt6
|
pip install PyQt6
|
||||||
|
else
|
||||||
|
echo "系统级 PyQt6 已安装,复制到虚拟环境中以供使用..."
|
||||||
|
cp -r /usr/lib/python3/dist-packages/PyQt6 "$VENVDIR/lib/$PYTHON_VERSION/site-packages/"
|
||||||
|
cp -r /usr/lib/python3/dist-packages/PyQt6-*.dist-info "$VENVDIR/lib/$PYTHON_VERSION/site-packages/"
|
||||||
|
cp -r /usr/lib/python3/dist-packages/PyQt6_sip-*.dist-info "$VENVDIR/lib/$PYTHON_VERSION/site-packages/" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REQ_FILE="$SCRIPT_DIR/requirements.txt"
|
REQ_FILE="$SCRIPT_DIR/requirements.txt"
|
||||||
|
|||||||
@@ -275,7 +275,6 @@ class ControlPage(QWidget):
|
|||||||
def _on_config_changed(self, config_instance):
|
def _on_config_changed(self, config_instance):
|
||||||
self._load_limits()
|
self._load_limits()
|
||||||
self._load_speeds()
|
self._load_speeds()
|
||||||
print(f"z-max-s:{self.move_speed_z}")
|
|
||||||
|
|
||||||
# ── 状态管理 ──────────────────────────────────────────
|
# ── 状态管理 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QPushButton,
|
QPushButton,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
|
QDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
@@ -28,6 +29,7 @@ import re
|
|||||||
from utils.wifi_manager import WifiManager
|
from utils.wifi_manager import WifiManager
|
||||||
from utils.floating_keyboard import FloatingKeyboard
|
from utils.floating_keyboard import FloatingKeyboard
|
||||||
from utils.get_bootstrap_icon import get_colored_icon, get_colored_pixmap
|
from utils.get_bootstrap_icon import get_colored_icon, get_colored_pixmap
|
||||||
|
from utils.modern_confirm import ModernConfirmDialog
|
||||||
|
|
||||||
|
|
||||||
class DragScrollArea(QScrollArea):
|
class DragScrollArea(QScrollArea):
|
||||||
@@ -344,7 +346,7 @@ class SettingPage(QWidget):
|
|||||||
color: #f2f2f2;
|
color: #f2f2f2;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #3f3f3f;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
QMessageBox QPushButton {
|
QMessageBox QPushButton {
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
@@ -1016,32 +1018,25 @@ class SettingPage(QWidget):
|
|||||||
self.settings_stack.addWidget(self._wrap_scroll(power_widget))
|
self.settings_stack.addWidget(self._wrap_scroll(power_widget))
|
||||||
|
|
||||||
def _confirm_reboot(self):
|
def _confirm_reboot(self):
|
||||||
reply = QMessageBox.question(
|
dlg = ModernConfirmDialog(self, "确认重启", "确定要重启系统吗?\n所有未保存的数据将丢失。", "question-circle.svg", confirm_text="重启", cancel_text="取消")
|
||||||
self, "确认重启",
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
"确定要重启系统吗?\n所有未保存的数据将丢失。",
|
# QTimer.singleShot(500, lambda: print("test sudo reboot"))
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
|
||||||
QMessageBox.StandardButton.No
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
|
||||||
self._styled_message(
|
self._styled_message(
|
||||||
"info", self, "重启",
|
"info", self, "重启",
|
||||||
"系统正在重启..."
|
"系统正在重启..."
|
||||||
)
|
)
|
||||||
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
|
|
||||||
|
|
||||||
def _confirm_shutdown(self):
|
def _confirm_shutdown(self):
|
||||||
reply = QMessageBox.question(
|
dlg = ModernConfirmDialog(self, "确认关机", "确定要关闭系统吗?\n关闭后需要手动重新开机。", "question-circle.svg", confirm_text="关机", cancel_text="取消")
|
||||||
self, "确认关机",
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
"确定要关闭系统吗?\n关闭后需要手动重新开机。",
|
# QTimer.singleShot(500, lambda: print("test sudo poweroff"))
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
|
||||||
QMessageBox.StandardButton.No
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
|
||||||
self._styled_message(
|
self._styled_message(
|
||||||
"info", self, "关机",
|
"info", self, "关机",
|
||||||
"系统正在关机..."
|
"系统正在关机..."
|
||||||
)
|
)
|
||||||
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
|
|
||||||
|
|
||||||
def init_todo_settings(self):
|
def init_todo_settings(self):
|
||||||
todo_widget = QWidget()
|
todo_widget = QWidget()
|
||||||
|
|||||||
@@ -71,14 +71,19 @@ class CardFrame(QFrame):
|
|||||||
class TempGauge(QWidget):
|
class TempGauge(QWidget):
|
||||||
"""温度计指示器"""
|
"""温度计指示器"""
|
||||||
|
|
||||||
def __init__(self, label="Tool", parent=None):
|
def __init__(self, label="Tool", temp_range=(0, 300), parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setFixedSize(100, 80)
|
self.setFixedSize(160, 90)
|
||||||
self._label = label
|
self._label = label
|
||||||
self._actual = 0.0
|
self._actual = 0.0
|
||||||
self._target = 0.0
|
self._target = 0.0
|
||||||
|
self._max_temp = temp_range[1]
|
||||||
|
self._min_temp = temp_range[0]
|
||||||
|
|
||||||
def set_value(self, actual, target):
|
def set_value(self, actual, target, temp_range=None):
|
||||||
|
if temp_range is not None:
|
||||||
|
self._max_temp = temp_range[1]
|
||||||
|
self._min_temp = temp_range[0]
|
||||||
self._actual = actual
|
self._actual = actual
|
||||||
self._target = target
|
self._target = target
|
||||||
self.update()
|
self.update()
|
||||||
@@ -89,14 +94,14 @@ class TempGauge(QWidget):
|
|||||||
w, h = self.width(), self.height()
|
w, h = self.width(), self.height()
|
||||||
|
|
||||||
# 背景条
|
# 背景条
|
||||||
bar_x, bar_w = 16, 20
|
bar_x, bar_w = 60, 20
|
||||||
bar_y, bar_h = 10, 56
|
bar_y, bar_h = 10, 56
|
||||||
p.setPen(QPen(QColor("#555555"), 1))
|
p.setPen(QPen(QColor("#555555"), 1))
|
||||||
p.setBrush(QBrush(QColor("#2a2a2a")))
|
p.setBrush(QBrush(QColor("#2a2a2a")))
|
||||||
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
|
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
|
||||||
|
|
||||||
# 填充柱(按温度比例,最高 300°C)
|
# 填充柱(按温度比例,最高 300°C)
|
||||||
ratio = min(max(self._actual / 300, 0), 1)
|
ratio = min(max((self._actual - self._min_temp) / (self._max_temp - self._min_temp), 0), 1)
|
||||||
fill_h = int((bar_h - 4) * ratio)
|
fill_h = int((bar_h - 4) * ratio)
|
||||||
if fill_h > 0:
|
if fill_h > 0:
|
||||||
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
|
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
|
||||||
@@ -108,28 +113,29 @@ class TempGauge(QWidget):
|
|||||||
|
|
||||||
# 目标值标记线
|
# 目标值标记线
|
||||||
if self._target > 0:
|
if self._target > 0:
|
||||||
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
|
tgt_y = bar_y + bar_h - int((bar_h - 4) * min((self._target - self._min_temp) / (self._max_temp - self._min_temp), 1))
|
||||||
p.setPen(QPen(QColor("#ffffff"), 2))
|
p.setPen(QPen(QColor("#888888"), 2))
|
||||||
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
|
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
|
||||||
|
|
||||||
# 文字
|
# 文字
|
||||||
font = QFont("sans-serif", 11, QFont.Weight.Bold)
|
font = QFont("sans-serif", 11, QFont.Weight.Bold)
|
||||||
p.setFont(font)
|
p.setFont(font)
|
||||||
p.setPen(QPen(QColor("#e0e0e0")))
|
temp_to_hex_soft = lambda t: "#{:02x}{:02x}{:02x}".format(*((lambda x: ((int(255*(0.3+0.7*(x/0.5)**0.8)), int(180*(x/0.5)**0.9), 255)if x < 0.5 else(255, int(180*(1-((x-0.5)/0.5)**1.2)), 80)))(max(0, min(t - self._min_temp, self._max_temp - self._min_temp))/(self._max_temp - self._min_temp))))
|
||||||
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
p.setPen(QPen(QColor(temp_to_hex_soft(self._actual))))
|
||||||
f"{self._actual:.0f}°")
|
p.drawText(0, int(bar_y + bar_h*(1-ratio)-12), w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||||
|
f"{self._actual:>5.1f}°C")
|
||||||
if self._target > 0:
|
if self._target > 0:
|
||||||
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
|
tgt_y = bar_y + bar_h - int((bar_h - 4) * min((self._target - self._min_temp) / (self._max_temp - self._min_temp), 1))
|
||||||
font2 = QFont("sans-serif", 10)
|
font2 = QFont("sans-serif", 10)
|
||||||
p.setFont(font2)
|
p.setFont(font2)
|
||||||
p.setPen(QPen(QColor("#888888")))
|
p.setPen(QPen(QColor("#888888")))
|
||||||
p.drawText(44, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
p.drawText(90, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||||
f"→ {self._target:.0f}°")
|
f"{self._target:.1f}°C")
|
||||||
|
|
||||||
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
|
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
|
||||||
p.setFont(font3)
|
p.setFont(font3)
|
||||||
p.setPen(QPen(QColor("#aaaaaa")))
|
p.setPen(QPen(QColor("#aaaaaa")))
|
||||||
p.drawText(0, h - 20, w, 20, Qt.AlignmentFlag.AlignCenter, self._label)
|
p.drawText(0, h - 20, w-20, 20, Qt.AlignmentFlag.AlignCenter, self._label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +161,8 @@ class StatusPage(QWidget):
|
|||||||
self.config_parser = ConfigParse()
|
self.config_parser = ConfigParse()
|
||||||
self.config_parser.config_changed.connect(self._on_config_changed)
|
self.config_parser.config_changed.connect(self._on_config_changed)
|
||||||
self.gcode_dir = self.config_parser.gcode_dir
|
self.gcode_dir = self.config_parser.gcode_dir
|
||||||
|
self.tool_temp_range = (self.config_parser.hotend_temp_range.get("min", 0), self.config_parser.hotend_temp_range.get("max", 300))
|
||||||
|
self.bed_temp_range = (self.config_parser.bed_temp_range.get("min", 0), self.config_parser.bed_temp_range.get("max", 120))
|
||||||
self._loaded_file = None
|
self._loaded_file = None
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
@@ -165,6 +173,8 @@ class StatusPage(QWidget):
|
|||||||
|
|
||||||
def _on_config_changed(self, config_instance):
|
def _on_config_changed(self, config_instance):
|
||||||
self.gcode_dir = self.config_parser.gcode_dir
|
self.gcode_dir = self.config_parser.gcode_dir
|
||||||
|
self.tool_temp_range = (self.config_parser.hotend_temp_range.get("min", 0), self.config_parser.hotend_temp_range.get("max", 300))
|
||||||
|
self.bed_temp_range = (self.config_parser.bed_temp_range.get("min", 0), self.config_parser.bed_temp_range.get("max", 120))
|
||||||
|
|
||||||
def fresh_status_valve(self):
|
def fresh_status_valve(self):
|
||||||
data = self.api_client.get_status()
|
data = self.api_client.get_status()
|
||||||
@@ -272,8 +282,8 @@ class StatusPage(QWidget):
|
|||||||
self._temp_card = CardFrame("温度")
|
self._temp_card = CardFrame("温度")
|
||||||
temp_row = QHBoxLayout()
|
temp_row = QHBoxLayout()
|
||||||
temp_row.setSpacing(8)
|
temp_row.setSpacing(8)
|
||||||
self._tool_gauge = TempGauge("喷头")
|
self._tool_gauge = TempGauge("喷头", self.tool_temp_range)
|
||||||
self._bed_gauge = TempGauge("热床")
|
self._bed_gauge = TempGauge("热床", self.bed_temp_range)
|
||||||
temp_row.addWidget(self._tool_gauge)
|
temp_row.addWidget(self._tool_gauge)
|
||||||
temp_row.addWidget(self._bed_gauge)
|
temp_row.addWidget(self._bed_gauge)
|
||||||
temp_row.addStretch()
|
temp_row.addStretch()
|
||||||
@@ -361,8 +371,8 @@ class StatusPage(QWidget):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
# 温度
|
# 温度
|
||||||
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target)
|
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target, self.tool_temp_range)
|
||||||
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target)
|
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target, self.bed_temp_range)
|
||||||
|
|
||||||
# G-code 模型加载与进度更新
|
# G-code 模型加载与进度更新
|
||||||
if self.file_name and self.file_name != "None":
|
if self.file_name and self.file_name != "None":
|
||||||
@@ -380,48 +390,3 @@ class StatusPage(QWidget):
|
|||||||
is_printing = self.state.startswith("Printing") or self.state.startswith("Paused")
|
is_printing = self.state.startswith("Printing") or self.state.startswith("Paused")
|
||||||
self.gcode_viewer.update_by_filepos(self.filepos, is_printing)
|
self.gcode_viewer.update_by_filepos(self.filepos, is_printing)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#TODO: Better Gcode Parser, this one is too slow for large files, need to optimize or use a separate thread to load
|
|
||||||
# def load_gcode_vertices(self, path):
|
|
||||||
# vertices = []
|
|
||||||
|
|
||||||
# x = 0
|
|
||||||
# y = 0
|
|
||||||
# z = 0
|
|
||||||
|
|
||||||
# with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
||||||
# for line in f:
|
|
||||||
# line = line.strip()
|
|
||||||
|
|
||||||
# if not line:
|
|
||||||
# continue
|
|
||||||
|
|
||||||
# if line.startswith("G0") or line.startswith("G1"):
|
|
||||||
# old_x = x
|
|
||||||
# old_y = y
|
|
||||||
# old_z = z
|
|
||||||
|
|
||||||
# mx = re.search(r"X([-0-9.]+)", line)
|
|
||||||
# my = re.search(r"Y([-0-9.]+)", line)
|
|
||||||
# mz = re.search(r"Z([-0-9.]+)", line)
|
|
||||||
|
|
||||||
# if mx:
|
|
||||||
# x = float(mx.group(1))
|
|
||||||
|
|
||||||
# if my:
|
|
||||||
# y = float(my.group(1))
|
|
||||||
|
|
||||||
# if mz:
|
|
||||||
# z = float(mz.group(1))
|
|
||||||
|
|
||||||
# vertices.append({
|
|
||||||
# "x1": old_x,
|
|
||||||
# "y1": old_y,
|
|
||||||
# "z1": old_z,
|
|
||||||
# "x2": x,
|
|
||||||
# "y2": y,
|
|
||||||
# "z2": z,
|
|
||||||
# })
|
|
||||||
|
|
||||||
# return vertices
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
|
import random
|
||||||
|
|
||||||
class AIOPrrintSystemAPI:
|
class AIOPrrintSystemAPI:
|
||||||
def __init__(self, api_url="http://127.0.0.1:5001/api/v1", api_key=""):
|
def __init__(self, api_url="http://127.0.0.1:5001/api/v1", api_key=""):
|
||||||
@@ -22,18 +23,18 @@ class AIOPrrintSystemAPI:
|
|||||||
# 'job': {
|
# 'job': {
|
||||||
# 'estimatedPrintTime': 1234,
|
# 'estimatedPrintTime': 1234,
|
||||||
# 'filament': {'length': 765, 'volume': 24356},
|
# 'filament': {'length': 765, 'volume': 24356},
|
||||||
# 'file': {'display_name': 'Test File','date': None, 'name': '20260508141659_085359c9908947bebcaa0fe7490641e8.gcode', 'origin': 'local', 'path': None, 'size': 1468987},
|
# 'file': {'display_name': 'Test File','date': None, 'name': '20260508141659_085359c9908947bebcaa0fe7490641e8.gcode', 'origin': 'local', 'path': None, 'size': 20934490},
|
||||||
# 'lastPrintTime': None,
|
# 'lastPrintTime': None,
|
||||||
# 'user': None
|
# 'user': None
|
||||||
# },
|
# },
|
||||||
# 'progress': {
|
# 'progress': {
|
||||||
# 'completion': 74.8,
|
# 'completion': random.uniform(0, 100.0),
|
||||||
# 'filepos': 1234,
|
# 'filepos': 20934490,
|
||||||
# 'printTime': 1235,
|
# 'printTime': random.randint(1234,54321),
|
||||||
# 'printTimeLeft': 6353,
|
# 'printTimeLeft': random.randint(1234,54321),
|
||||||
# 'printTimeLeftOrigin': 5366
|
# 'printTimeLeftOrigin': 5366
|
||||||
# },
|
# },
|
||||||
# 'state': 'Operational'
|
# 'state': 'Printing'
|
||||||
# },
|
# },
|
||||||
# 'status': {
|
# 'status': {
|
||||||
# 'sd': {'ready': False},
|
# 'sd': {'ready': False},
|
||||||
@@ -44,19 +45,19 @@ class AIOPrrintSystemAPI:
|
|||||||
# 'closedOrError': False,
|
# 'closedOrError': False,
|
||||||
# 'error': False,
|
# 'error': False,
|
||||||
# 'finishing': False,
|
# 'finishing': False,
|
||||||
# 'operational': True,
|
# 'operational': False,
|
||||||
# 'paused': False,
|
# 'paused': False,
|
||||||
# 'pausing': False,
|
# 'pausing': False,
|
||||||
# 'printing': False,
|
# 'printing': True,
|
||||||
# 'ready': True,
|
# 'ready': True,
|
||||||
# 'resuming': False,
|
# 'resuming': False,
|
||||||
# 'sdReady': False
|
# 'sdReady': False
|
||||||
# },
|
# },
|
||||||
# 'text': 'Operational test'
|
# 'text': 'Printing'
|
||||||
# },
|
# },
|
||||||
# 'temperature': {
|
# 'temperature': {
|
||||||
# 'bed': {'actual': 85, 'offset': 0, 'target': 56},
|
# 'bed': {'actual': random.randint(0,100), 'offset': 0, 'target': random.randint(0,100)},
|
||||||
# 'tool0': {'actual': 0.0, 'offset': 0, 'target': 340}
|
# 'tool0': {'actual': random.randint(0,300), 'offset': 0, 'target': random.randint(0,300)}
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class ConfigParse(QObject):
|
|||||||
self.gcode_dir = None
|
self.gcode_dir = None
|
||||||
self.move_axis_area = None
|
self.move_axis_area = None
|
||||||
self.move_max_speed = None
|
self.move_max_speed = None
|
||||||
|
self.hotend_temp_range = None
|
||||||
|
self.bed_temp_range = None
|
||||||
self.config = self._load_config()
|
self.config = self._load_config()
|
||||||
self._parse_config()
|
self._parse_config()
|
||||||
|
|
||||||
@@ -68,3 +70,5 @@ class ConfigParse(QObject):
|
|||||||
self.move_max_speed = self.config.get("move_max_speed", {"x": 3000, "y": 3000, "z": 200})
|
self.move_max_speed = self.config.get("move_max_speed", {"x": 3000, "y": 3000, "z": 200})
|
||||||
self.home_positions = self.config.get("home_positions", {"x": 134, "y": 123, "z": 10})
|
self.home_positions = self.config.get("home_positions", {"x": 134, "y": 123, "z": 10})
|
||||||
self.move_max_speed = self.config.get("move_max_speed", None)
|
self.move_max_speed = self.config.get("move_max_speed", None)
|
||||||
|
self.hotend_temp_range = self.config.get("hotend_temp_range", {"min": 0, "max": 300})
|
||||||
|
self.bed_temp_range = self.config.get("bed_temp_range", {"min": 0, "max": 120})
|
||||||
@@ -17,6 +17,7 @@ class GCodeParseWorker(QThread):
|
|||||||
points = []
|
points = []
|
||||||
colors = []
|
colors = []
|
||||||
type_segments = {}
|
type_segments = {}
|
||||||
|
segment_zs = {}
|
||||||
type_visibility = {}
|
type_visibility = {}
|
||||||
|
|
||||||
x = y = z = e = 0.0
|
x = y = z = e = 0.0
|
||||||
@@ -25,6 +26,7 @@ class GCodeParseWorker(QThread):
|
|||||||
current_segment_type = 'OTHER'
|
current_segment_type = 'OTHER'
|
||||||
segment_start = 0
|
segment_start = 0
|
||||||
type_visibility['TRAVEL'] = False
|
type_visibility['TRAVEL'] = False
|
||||||
|
|
||||||
relative_e = False
|
relative_e = False
|
||||||
|
|
||||||
min_x = min_y = min_z = float('inf')
|
min_x = min_y = min_z = float('inf')
|
||||||
@@ -32,11 +34,16 @@ class GCodeParseWorker(QThread):
|
|||||||
|
|
||||||
current_offset = 0
|
current_offset = 0
|
||||||
max_z_seen = -999.0
|
max_z_seen = -999.0
|
||||||
|
z_deltas = []
|
||||||
|
last_layer_z = None
|
||||||
layer_map = [(0, 0)]
|
layer_map = [(0, 0)]
|
||||||
|
layer_grids = {}
|
||||||
|
segment_visibility = {}
|
||||||
|
|
||||||
def add_segment(t_name, start, length):
|
def add_segment(t_name, start, length, z_val):
|
||||||
if length > 0:
|
if length > 0:
|
||||||
type_segments.setdefault(t_name, []).append((start, length))
|
type_segments.setdefault(t_name, []).append((start, length))
|
||||||
|
segment_zs[(start, length)] = z_val
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.filepath, 'rb') as f:
|
with open(self.filepath, 'rb') as f:
|
||||||
@@ -113,7 +120,10 @@ class GCodeParseWorker(QThread):
|
|||||||
|
|
||||||
if seg_type != current_segment_type:
|
if seg_type != current_segment_type:
|
||||||
if vertex_idx > segment_start:
|
if vertex_idx > segment_start:
|
||||||
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
seg_len = vertex_idx - segment_start
|
||||||
|
add_segment(current_segment_type, segment_start, seg_len, z)
|
||||||
|
segment_visibility[(segment_start, seg_len)] = (current_segment_type != 'TRAVEL')
|
||||||
|
# add_segment(current_segment_type, segment_start, vertex_idx - segment_start, z)
|
||||||
current_segment_type = seg_type
|
current_segment_type = seg_type
|
||||||
segment_start = vertex_idx
|
segment_start = vertex_idx
|
||||||
|
|
||||||
@@ -127,6 +137,18 @@ class GCodeParseWorker(QThread):
|
|||||||
if new_z > max_z_seen and is_extrusion:
|
if new_z > max_z_seen and is_extrusion:
|
||||||
max_z_seen = new_z
|
max_z_seen = new_z
|
||||||
layer_map.append((current_offset, vertex_idx))
|
layer_map.append((current_offset, vertex_idx))
|
||||||
|
if last_layer_z is not None:
|
||||||
|
dz = round(new_z - last_layer_z, 4)
|
||||||
|
if 0.01 < dz < 2.0:
|
||||||
|
z_deltas.append(dz)
|
||||||
|
last_layer_z = new_z
|
||||||
|
|
||||||
|
# 建立二维占据网络
|
||||||
|
if is_extrusion:
|
||||||
|
grid_z = round(new_z, 2)
|
||||||
|
gx = int(new_x / 2.0)
|
||||||
|
gy = int(new_y / 2.0)
|
||||||
|
layer_grids.setdefault(grid_z,set()).add((gx, gy))
|
||||||
|
|
||||||
points.extend([x, y, z, new_x, new_y, new_z])
|
points.extend([x, y, z, new_x, new_y, new_z])
|
||||||
colors.extend([*c, *c])
|
colors.extend([*c, *c])
|
||||||
@@ -144,12 +166,47 @@ class GCodeParseWorker(QThread):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if vertex_idx > segment_start:
|
if vertex_idx > segment_start:
|
||||||
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
# add_segment(current_segment_type, segment_start, vertex_idx - segment_start, z)
|
||||||
|
seg_len = vertex_idx - segment_start
|
||||||
|
add_segment(current_segment_type, segment_start, seg_len, z)
|
||||||
|
segment_visibility[(segment_start, seg_len)] = True
|
||||||
|
|
||||||
|
# 判断哪些segment在内部
|
||||||
|
for type_name, segments in type_segments.items():
|
||||||
|
if type_name in ('WALL-INNER', 'SUPPORT', 'FILL'):
|
||||||
|
for start, length in segments:
|
||||||
|
try:
|
||||||
|
px = points[start*3]
|
||||||
|
py = points[start*3+1]
|
||||||
|
pz = round(points[start*3+2], 2)
|
||||||
|
gx = int(px / 2.0)
|
||||||
|
gy = int(py / 2.0)
|
||||||
|
gird = layer_grids.get(pz, set())
|
||||||
|
if not gird:
|
||||||
|
continue
|
||||||
|
neighbors = 0
|
||||||
|
# for ox in range(-2,3):
|
||||||
|
# for oy in range(-2,3):
|
||||||
|
# if (gx + ox, gy + oy) in gird:
|
||||||
|
# neighbors += 1
|
||||||
|
for ox, oy in [(1,0), (-1,0), (0,1), (0,-1)]:
|
||||||
|
if (gx+ox, gy+oy) in gird:
|
||||||
|
neighbors += 1
|
||||||
|
if neighbors >= 4:
|
||||||
|
segment_visibility[(start, length)] = False
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
cx = (min_x + max_x) / 2.0 if max_x >= min_x else 110.0
|
cx = (min_x + max_x) / 2.0 if max_x >= min_x else 110.0
|
||||||
cy = (min_y + max_y) / 2.0 if max_y >= min_y else 110.0
|
cy = (min_y + max_y) / 2.0 if max_y >= min_y else 110.0
|
||||||
cz = (min_z + max_z) / 2.0 if max_z >= min_z else 0.0
|
cz = (min_z + max_z) / 2.0 if max_z >= min_z else 0.0
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
layer_height = 0.2
|
||||||
|
if z_deltas:
|
||||||
|
counter = Counter(z_deltas)
|
||||||
|
layer_height = counter.most_common(1)[0][0]
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'vertices': np.array(points, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
'vertices': np.array(points, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||||
'colors': np.array(colors, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
'colors': np.array(colors, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||||
@@ -159,7 +216,10 @@ class GCodeParseWorker(QThread):
|
|||||||
'center_z': cz,
|
'center_z': cz,
|
||||||
'type_segments': type_segments,
|
'type_segments': type_segments,
|
||||||
'type_visibility': type_visibility,
|
'type_visibility': type_visibility,
|
||||||
'layer_map': layer_map
|
'layer_map': layer_map,
|
||||||
|
'segment_visibility': segment_visibility,
|
||||||
|
'segment_zs': segment_zs,
|
||||||
|
'layer_height': layer_height
|
||||||
}
|
}
|
||||||
self.finished.emit(result)
|
self.finished.emit(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -281,6 +341,12 @@ class GCodeViewerWidget(QOpenGLWidget):
|
|||||||
self.aColor_location = None
|
self.aColor_location = None
|
||||||
self.uMVP_location = None
|
self.uMVP_location = None
|
||||||
|
|
||||||
|
# 简化参数
|
||||||
|
self.enable_lod = True
|
||||||
|
self.lod_factor = 0.0
|
||||||
|
self.visible_top_layers = 0
|
||||||
|
self.visible_bottom_layers = 0
|
||||||
|
|
||||||
# ── 公开接口 ──
|
# ── 公开接口 ──
|
||||||
def load_gcode(self, filepath: str):
|
def load_gcode(self, filepath: str):
|
||||||
if hasattr(self, '_worker') and self._worker.isRunning():
|
if hasattr(self, '_worker') and self._worker.isRunning():
|
||||||
@@ -302,8 +368,13 @@ class GCodeViewerWidget(QOpenGLWidget):
|
|||||||
self.center_y = result['center_y']
|
self.center_y = result['center_y']
|
||||||
self.center_z = result['center_z']
|
self.center_z = result['center_z']
|
||||||
self.type_segments = result['type_segments']
|
self.type_segments = result['type_segments']
|
||||||
|
self.segment_zs = result['segment_zs']
|
||||||
self.type_visibility = result['type_visibility']
|
self.type_visibility = result['type_visibility']
|
||||||
self.layer_map = result['layer_map']
|
self.layer_map = result['layer_map']
|
||||||
|
self.segment_visibility = result['segment_visibility']
|
||||||
|
|
||||||
|
self.min_z = np.min(self.vertices[2::3]) if self.vertex_count > 0 else 0.0
|
||||||
|
self.max_z = np.max(self.vertices[2::3]) if self.vertex_count > 0 else 0.0
|
||||||
|
|
||||||
self.vbo_ready = False
|
self.vbo_ready = False
|
||||||
# 初始时先不显示,让 update_by_filepos 决定,或者如果是0则自动更新
|
# 初始时先不显示,让 update_by_filepos 决定,或者如果是0则自动更新
|
||||||
@@ -311,6 +382,7 @@ class GCodeViewerWidget(QOpenGLWidget):
|
|||||||
target = 0
|
target = 0
|
||||||
if self.layer_map:
|
if self.layer_map:
|
||||||
target = self.layer_map[-1][1] # 全显
|
target = self.layer_map[-1][1] # 全显
|
||||||
|
self.layer_height = result.get('layer_height', 0.2)
|
||||||
|
|
||||||
self.progress_vertices = target
|
self.progress_vertices = target
|
||||||
self.last_reported_filepos = -1
|
self.last_reported_filepos = -1
|
||||||
@@ -433,24 +505,88 @@ class GCodeViewerWidget(QOpenGLWidget):
|
|||||||
# 因此通过动态加粗像素级线宽来性能无损地模拟出“体积感”)
|
# 因此通过动态加粗像素级线宽来性能无损地模拟出“体积感”)
|
||||||
for pass_idx in range(2):
|
for pass_idx in range(2):
|
||||||
if pass_idx == 0:
|
if pass_idx == 0:
|
||||||
|
if self.enable_lod:
|
||||||
|
gl.glLineWidth(3.0) # 底线宽度(加大以模拟体积轮廓)
|
||||||
|
else:
|
||||||
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
|
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
|
||||||
self.shader_program.setUniformValue(self.uDarken_location, 0.8) # 加深颜色至 40% 亮度
|
self.shader_program.setUniformValue(self.uDarken_location, 0.8) # 加深颜色至 40% 亮度
|
||||||
|
else:
|
||||||
|
if self.enable_lod:
|
||||||
|
gl.glLineWidth(1.5) # 主体宽度(加大以模拟线条厚度)
|
||||||
else:
|
else:
|
||||||
gl.glLineWidth(3.0) # 主体宽度(加大以模拟线条厚度)
|
gl.glLineWidth(3.0) # 主体宽度(加大以模拟线条厚度)
|
||||||
self.shader_program.setUniformValue(self.uDarken_location, 1.0) # 保持原色
|
self.shader_program.setUniformValue(self.uDarken_location, 1.0) # 保持原色
|
||||||
|
|
||||||
# 按类型分段绘制
|
# 按类型分段绘制
|
||||||
|
# for type_name, segments in self.type_segments.items():
|
||||||
|
# if not self.type_visibility.get(type_name, True):
|
||||||
|
# continue
|
||||||
|
# for start, length in segments:
|
||||||
|
# if start >= self.progress_vertices:
|
||||||
|
# continue
|
||||||
|
# end = start + length
|
||||||
|
# visible_start = start
|
||||||
|
# visible_count = length
|
||||||
|
# if end > self.progress_vertices:
|
||||||
|
# visible_count = self.progress_vertices - start
|
||||||
|
# if visible_count > 0:
|
||||||
|
# gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)
|
||||||
|
|
||||||
|
# 按类型分段绘制(带LOD)
|
||||||
for type_name, segments in self.type_segments.items():
|
for type_name, segments in self.type_segments.items():
|
||||||
if not self.type_visibility.get(type_name, True):
|
if not self.type_visibility.get(type_name, True):
|
||||||
continue
|
continue
|
||||||
|
# 不同类型的简化倍率
|
||||||
|
simplify_mul = 1
|
||||||
|
if type_name == 'FILL':
|
||||||
|
simplify_mul = 3
|
||||||
|
elif type_name == 'SUPPORT':
|
||||||
|
simplify_mul = 4
|
||||||
|
elif type_name == 'TRAVEL':
|
||||||
|
simplify_mul = 8
|
||||||
|
# 根据缩放动态增强简化
|
||||||
|
zoom_mul = max(1.0, abs(self.view_zoom) / 250.0)
|
||||||
|
# 最终步进
|
||||||
|
lod_step = 1
|
||||||
|
if self.enable_lod:
|
||||||
|
lod_step = int(max(1, self.lod_factor * simplify_mul * zoom_mul))
|
||||||
|
seg_index = 0
|
||||||
for start, length in segments:
|
for start, length in segments:
|
||||||
|
if not self.segment_visibility.get((start, length), True):
|
||||||
|
continue
|
||||||
|
if type_name in ('WALL-INNER', 'SUPPORT', 'FILL'):
|
||||||
|
seg_z = self.segment_zs.get((start, length), self.center_z)
|
||||||
|
top_limit = self.max_z - self.visible_top_layers * self.layer_height if self.visible_top_layers > 0 else self.min_z - 1.0
|
||||||
|
bottom_limit = self.min_z + self.visible_bottom_layers * self.layer_height if self.visible_bottom_layers > 0 else self.max_z + 1.0
|
||||||
|
if bottom_limit <= seg_z <= top_limit:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seg_index += 1
|
||||||
|
# LOD抽样
|
||||||
|
if lod_step > 1:
|
||||||
|
if (seg_index % lod_step) != 0:
|
||||||
|
continue
|
||||||
if start >= self.progress_vertices:
|
if start >= self.progress_vertices:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
end = start + length
|
end = start + length
|
||||||
visible_start = start
|
visible_start = start
|
||||||
visible_count = length
|
visible_count = length
|
||||||
if end > self.progress_vertices:
|
if end > self.progress_vertices:
|
||||||
visible_count = self.progress_vertices - start
|
visible_count = self.progress_vertices - start
|
||||||
|
if visible_count <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 超远距离时进一步减少绘制长度
|
||||||
|
if self.enable_lod:
|
||||||
|
if type_name in ('FILL','SUPPORT','WALL-INNER'):
|
||||||
|
if abs(self.view_zoom) > 400:
|
||||||
|
if(start // 2) % 3 != 0:
|
||||||
|
continue
|
||||||
|
if abs(self.view_zoom) > 700:
|
||||||
|
if(start // 2) % 6 != 0:
|
||||||
|
continue
|
||||||
|
# visible_count = visible_count // 2
|
||||||
if visible_count > 0:
|
if visible_count > 0:
|
||||||
gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)
|
gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)
|
||||||
|
|
||||||
|
|||||||
601
utils/gcode_viewer_deepseek.py
Normal file
601
utils/gcode_viewer_deepseek.py
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
import bisect
|
||||||
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||||
|
from PyQt6.QtCore import Qt, QEvent, QThread, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QTouchEvent, QSurfaceFormat, QMatrix4x4, QVector4D
|
||||||
|
from PyQt6.QtOpenGL import QOpenGLShaderProgram, QOpenGLShader, QOpenGLBuffer
|
||||||
|
|
||||||
|
class GCodeParseWorker(QThread):
|
||||||
|
finished = pyqtSignal(dict)
|
||||||
|
|
||||||
|
def __init__(self, filepath, type_map, default_colors, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.filepath = filepath
|
||||||
|
self.TYPE_MAP = type_map
|
||||||
|
self.DEFAULT_COLORS = default_colors
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
points = []
|
||||||
|
colors = []
|
||||||
|
type_segments = {}
|
||||||
|
type_visibility = {'TRAVEL': False}
|
||||||
|
segment_types = [] # 每条线段对应的类型名称(长度为线段数)
|
||||||
|
|
||||||
|
x = y = z = e = 0.0
|
||||||
|
vertex_idx = 0
|
||||||
|
feature_type = 'OTHER'
|
||||||
|
current_segment_type = 'OTHER'
|
||||||
|
segment_start = 0
|
||||||
|
relative_e = False
|
||||||
|
|
||||||
|
min_x = min_y = min_z = float('inf')
|
||||||
|
max_x = max_y = max_z = float('-inf')
|
||||||
|
|
||||||
|
current_offset = 0
|
||||||
|
max_z_seen = -999.0
|
||||||
|
layer_map = [(0, 0)]
|
||||||
|
|
||||||
|
lod_steps = [2, 4]
|
||||||
|
lod_levels = {1: None}
|
||||||
|
for s in lod_steps:
|
||||||
|
lod_levels[s] = {'points': [], 'colors': [], 'count': 0}
|
||||||
|
|
||||||
|
grid_size = 10
|
||||||
|
grid_segments = {}
|
||||||
|
|
||||||
|
def add_segment(t_name, start, length):
|
||||||
|
if length > 0:
|
||||||
|
type_segments.setdefault(t_name, []).append((start, length))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.filepath, 'rb') as f:
|
||||||
|
for line_bytes in f:
|
||||||
|
current_offset += len(line_bytes)
|
||||||
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.startswith('M82'):
|
||||||
|
relative_e = False
|
||||||
|
continue
|
||||||
|
if line.startswith('M83'):
|
||||||
|
relative_e = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(';'):
|
||||||
|
if line.startswith(';TYPE:'):
|
||||||
|
raw_type = line.split(':', 1)[1].strip()
|
||||||
|
else:
|
||||||
|
raw_type = line[1:].strip()
|
||||||
|
if raw_type not in self.TYPE_MAP and not any(k in raw_type.lower() for k in ('perimeter', 'infill', 'material', 'skirt/brim')):
|
||||||
|
continue
|
||||||
|
if 'Skirt/Brim' in raw_type:
|
||||||
|
raw_type = 'Skirt'
|
||||||
|
new_type = self.TYPE_MAP.get(raw_type, 'OTHER')
|
||||||
|
if new_type != feature_type:
|
||||||
|
feature_type = new_type
|
||||||
|
if feature_type not in type_visibility:
|
||||||
|
type_visibility[feature_type] = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(('G0', 'G1')):
|
||||||
|
new_x, new_y, new_z = x, y, z
|
||||||
|
e_val = 0.0
|
||||||
|
has_e = False
|
||||||
|
parts = line.split(';')[0].split()
|
||||||
|
for p in parts[1:]:
|
||||||
|
try:
|
||||||
|
if p.startswith('X'): new_x = float(p[1:])
|
||||||
|
elif p.startswith('Y'): new_y = float(p[1:])
|
||||||
|
elif p.startswith('Z'): new_z = float(p[1:])
|
||||||
|
elif p.startswith('E'):
|
||||||
|
e_val = float(p[1:])
|
||||||
|
has_e = True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if new_x == x and new_y == y and new_z == z:
|
||||||
|
if has_e:
|
||||||
|
if relative_e: e += e_val
|
||||||
|
else: e = e_val
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_extrusion = False
|
||||||
|
if has_e:
|
||||||
|
if relative_e:
|
||||||
|
is_extrusion = e_val > 0
|
||||||
|
new_e = e + e_val
|
||||||
|
else:
|
||||||
|
is_extrusion = e_val > e
|
||||||
|
new_e = e_val
|
||||||
|
else:
|
||||||
|
new_e = e
|
||||||
|
|
||||||
|
if is_extrusion:
|
||||||
|
seg_type = feature_type
|
||||||
|
c = self.DEFAULT_COLORS.get(feature_type, self.DEFAULT_COLORS['OTHER'])
|
||||||
|
else:
|
||||||
|
seg_type = 'TRAVEL'
|
||||||
|
c = self.DEFAULT_COLORS['TRAVEL']
|
||||||
|
if 'TRAVEL' not in type_visibility:
|
||||||
|
type_visibility['TRAVEL'] = False
|
||||||
|
|
||||||
|
# 记录当前线段的类型
|
||||||
|
segment_types.append(seg_type)
|
||||||
|
|
||||||
|
if seg_type != current_segment_type:
|
||||||
|
if vertex_idx > segment_start:
|
||||||
|
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||||
|
current_segment_type = seg_type
|
||||||
|
segment_start = vertex_idx
|
||||||
|
|
||||||
|
if new_x < min_x: min_x = new_x
|
||||||
|
if new_x > max_x: max_x = new_x
|
||||||
|
if new_y < min_y: min_y = new_y
|
||||||
|
if new_y > max_y: max_y = new_y
|
||||||
|
if new_z < min_z: min_z = new_z
|
||||||
|
if new_z > max_z: max_z = new_z
|
||||||
|
|
||||||
|
if new_z > max_z_seen and is_extrusion:
|
||||||
|
max_z_seen = new_z
|
||||||
|
layer_map.append((current_offset, vertex_idx))
|
||||||
|
|
||||||
|
points.extend([x, y, z, new_x, new_y, new_z])
|
||||||
|
colors.extend([*c, *c])
|
||||||
|
|
||||||
|
for step in lod_steps:
|
||||||
|
if vertex_idx % (step * 2) == 0:
|
||||||
|
lod_levels[step]['points'].extend([x, y, z, new_x, new_y, new_z])
|
||||||
|
lod_levels[step]['colors'].extend([*c, *c])
|
||||||
|
lod_levels[step]['count'] += 2
|
||||||
|
|
||||||
|
vertex_idx += 2
|
||||||
|
x, y, z, e = new_x, new_y, new_z, new_e
|
||||||
|
|
||||||
|
elif line.startswith('G92'):
|
||||||
|
parts = line.split(';')[0].split()
|
||||||
|
for p in parts[1:]:
|
||||||
|
try:
|
||||||
|
if p.startswith('E'): e = float(p[1:])
|
||||||
|
elif p.startswith('X'): x = float(p[1:])
|
||||||
|
elif p.startswith('Y'): y = float(p[1:])
|
||||||
|
elif p.startswith('Z'): z = float(p[1:])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if vertex_idx > segment_start:
|
||||||
|
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||||
|
|
||||||
|
cx = (min_x + max_x) / 2.0 if max_x >= min_x else 110.0
|
||||||
|
cy = (min_y + max_y) / 2.0 if max_y >= min_y else 110.0
|
||||||
|
cz = (min_z + max_z) / 2.0 if max_z >= min_z else 0.0
|
||||||
|
|
||||||
|
if vertex_idx > 0 and max_x > min_x and max_y > min_y:
|
||||||
|
dx = (max_x - min_x) / grid_size
|
||||||
|
dy = (max_y - min_y) / grid_size
|
||||||
|
for i in range(grid_size):
|
||||||
|
for j in range(grid_size):
|
||||||
|
grid_segments[(i, j)] = []
|
||||||
|
# 填充网格,同时每个格子存储 (线段起始顶点索引, 类型)
|
||||||
|
for seg_idx in range(0, vertex_idx, 2):
|
||||||
|
px = points[seg_idx * 3]
|
||||||
|
py = points[seg_idx * 3 + 1]
|
||||||
|
gx = min(int((px - min_x) / dx), grid_size - 1)
|
||||||
|
gy = min(int((py - min_y) / dy), grid_size - 1)
|
||||||
|
seg_type = segment_types[seg_idx // 2]
|
||||||
|
grid_segments[(gx, gy)].append((seg_idx, seg_type))
|
||||||
|
|
||||||
|
lod_levels[1] = {'points': points, 'colors': colors, 'count': vertex_idx}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'lod_levels': lod_levels,
|
||||||
|
'vertex_count': vertex_idx,
|
||||||
|
'center_x': cx,
|
||||||
|
'center_y': cy,
|
||||||
|
'center_z': cz,
|
||||||
|
'type_segments': type_segments,
|
||||||
|
'type_visibility': type_visibility,
|
||||||
|
'layer_map': layer_map,
|
||||||
|
'grid_segments': grid_segments,
|
||||||
|
'grid_size': grid_size,
|
||||||
|
'bbox': (min_x, max_x, min_y, max_y, min_z, max_z)
|
||||||
|
}
|
||||||
|
self.finished.emit(result)
|
||||||
|
except Exception as e:
|
||||||
|
print("ParseGCode Error:", e)
|
||||||
|
self.finished.emit({})
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeViewerWidget(QOpenGLWidget):
|
||||||
|
TYPE_MAP = {
|
||||||
|
'External perimeter': 'WALL-OUTER',
|
||||||
|
'Perimeter': 'WALL-INNER',
|
||||||
|
'Overhang perimeter': 'WALL-OUTER',
|
||||||
|
'Solid infill': 'SKIN',
|
||||||
|
'Top solid infill': 'SKIN',
|
||||||
|
'Bridge infill': 'SKIN',
|
||||||
|
'Internal infill': 'FILL',
|
||||||
|
'Support material': 'SUPPORT',
|
||||||
|
'Support material interface': 'SUPPORT-INTERFACE',
|
||||||
|
'Skirt': 'SKIRT',
|
||||||
|
'Brim': 'SKIRT',
|
||||||
|
'Custom': 'OTHER',
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_COLORS = {
|
||||||
|
'WALL-OUTER': (0.92, 0.55, 0.22),
|
||||||
|
'WALL-INNER': (0.25, 0.50, 0.81),
|
||||||
|
'FILL': (0.80, 0.75, 0.29),
|
||||||
|
'SKIN': (0.62, 0.38, 0.70),
|
||||||
|
'SUPPORT': (0.34, 0.70, 0.34),
|
||||||
|
'SUPPORT-INTERFACE': (0.17, 0.42, 0.17),
|
||||||
|
'SKIRT': (0.00, 1.00, 1.00),
|
||||||
|
'OTHER': (0.67, 0.67, 0.67),
|
||||||
|
'TRAVEL': (0.25, 0.31, 0.38),
|
||||||
|
}
|
||||||
|
|
||||||
|
VERTEX_SHADER = """
|
||||||
|
attribute vec3 aPos;
|
||||||
|
attribute vec3 aColor;
|
||||||
|
varying vec3 vColor;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
void main() {
|
||||||
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||||
|
vColor = aColor;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
FRAGMENT_SHADER = """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec3 vColor;
|
||||||
|
uniform float uDarken;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(vColor * uDarken, 1.0);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
fmt = QSurfaceFormat()
|
||||||
|
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGLES)
|
||||||
|
fmt.setVersion(2, 0)
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumSize(400, 300)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True)
|
||||||
|
self.setFormat(fmt)
|
||||||
|
|
||||||
|
self.lod_levels = {}
|
||||||
|
self.vbos = {}
|
||||||
|
self.vertex_count = 0
|
||||||
|
|
||||||
|
self.type_segments = {}
|
||||||
|
self.type_visibility = {}
|
||||||
|
|
||||||
|
self.view_rot_x = -60.0
|
||||||
|
self.view_rot_z = 45.0
|
||||||
|
self.view_zoom = -250.0
|
||||||
|
self.view_trans_x = 0.0
|
||||||
|
self.view_trans_y = 0.0
|
||||||
|
|
||||||
|
self.progress_vertices = 0
|
||||||
|
self.layer_map = [(0, 0)]
|
||||||
|
|
||||||
|
self.center_x = 110.0
|
||||||
|
self.center_y = 110.0
|
||||||
|
self.center_z = 0.0
|
||||||
|
|
||||||
|
self.grid_segments = {} # 格子 -> [(seg_idx, type_name)]
|
||||||
|
self.grid_size = 10
|
||||||
|
self.bbox = (0, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.last_mouse_pos = None
|
||||||
|
self._touch_points = {}
|
||||||
|
self._pinch_start_dist = 0.0
|
||||||
|
self._pinch_start_zoom = 0.0
|
||||||
|
self._pinch_start_center = None
|
||||||
|
self._pinch_start_trans = (0.0, 0.0)
|
||||||
|
self._ignore_wheel = False
|
||||||
|
self._rot_sensitivity = 0.1
|
||||||
|
|
||||||
|
self.shader_program = None
|
||||||
|
self.aPos_location = None
|
||||||
|
self.aColor_location = None
|
||||||
|
self.uMVP_location = None
|
||||||
|
self.uDarken_location = None
|
||||||
|
|
||||||
|
def load_gcode(self, filepath: str):
|
||||||
|
if hasattr(self, '_worker') and self._worker.isRunning():
|
||||||
|
self._worker.terminate()
|
||||||
|
self._worker.wait()
|
||||||
|
self._worker = GCodeParseWorker(filepath, self.TYPE_MAP, self.DEFAULT_COLORS)
|
||||||
|
self._worker.finished.connect(self._on_parse_finished)
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
|
def _on_parse_finished(self, result: dict):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
self.lod_levels = result['lod_levels']
|
||||||
|
self.vertex_count = result['vertex_count']
|
||||||
|
self.center_x = result['center_x']
|
||||||
|
self.center_y = result['center_y']
|
||||||
|
self.center_z = result['center_z']
|
||||||
|
self.type_segments = result['type_segments']
|
||||||
|
self.type_visibility = result['type_visibility']
|
||||||
|
self.layer_map = result['layer_map']
|
||||||
|
self.grid_segments = result['grid_segments']
|
||||||
|
self.grid_size = result['grid_size']
|
||||||
|
self.bbox = result['bbox']
|
||||||
|
self.progress_vertices = self.vertex_count
|
||||||
|
self.vbos.clear()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update_by_filepos(self, filepos: int, is_printing: bool = True):
|
||||||
|
if not self.layer_map:
|
||||||
|
return
|
||||||
|
if not is_printing:
|
||||||
|
target_vertices = self.vertex_count
|
||||||
|
else:
|
||||||
|
keys = [item[0] for item in self.layer_map]
|
||||||
|
idx = bisect.bisect_right(keys, filepos)
|
||||||
|
target_vertices = 0 if idx == 0 else self.layer_map[idx-1][1]
|
||||||
|
if target_vertices != self.progress_vertices:
|
||||||
|
self.progress_vertices = target_vertices
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update_switch(self, type_name: str, visible: bool):
|
||||||
|
if type_name in self.type_visibility:
|
||||||
|
self.type_visibility[type_name] = visible
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def set_view_angles(self, rot_x: float, rot_z: float, zoom: float = None):
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, rot_x))
|
||||||
|
self.view_rot_z = rot_z
|
||||||
|
if zoom is not None:
|
||||||
|
self.view_zoom = zoom
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def initializeGL(self):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glClearColor(0.15, 0.15, 0.15, 1.0)
|
||||||
|
gl.glEnable(gl.GL_DEPTH_TEST)
|
||||||
|
self.shader_program = QOpenGLShaderProgram()
|
||||||
|
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, self.VERTEX_SHADER)
|
||||||
|
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, self.FRAGMENT_SHADER)
|
||||||
|
self.shader_program.link()
|
||||||
|
self.aPos_location = self.shader_program.attributeLocation("aPos")
|
||||||
|
self.aColor_location = self.shader_program.attributeLocation("aColor")
|
||||||
|
self.uMVP_location = self.shader_program.uniformLocation("uMVP")
|
||||||
|
self.uDarken_location = self.shader_program.uniformLocation("uDarken")
|
||||||
|
|
||||||
|
def resizeGL(self, w, h):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glViewport(0, 0, w, h)
|
||||||
|
|
||||||
|
def _build_mvp(self):
|
||||||
|
mat = QMatrix4x4()
|
||||||
|
aspect = self.width() / self.height() if self.height() else 1
|
||||||
|
mat.perspective(45.0, aspect, 1.0, 1000.0)
|
||||||
|
mat.translate(self.view_trans_x, self.view_trans_y, self.view_zoom)
|
||||||
|
mat.rotate(self.view_rot_x, 1.0, 0.0, 0.0)
|
||||||
|
mat.rotate(self.view_rot_z, 0.0, 0.0, 1.0)
|
||||||
|
mat.translate(-self.center_x, -self.center_y, -self.center_z)
|
||||||
|
return mat
|
||||||
|
|
||||||
|
def _is_tile_visible(self, gx, gy, mvp_mat):
|
||||||
|
min_x, max_x, min_y, max_y, min_z, max_z = self.bbox
|
||||||
|
if max_x <= min_x or max_y <= min_y:
|
||||||
|
return True
|
||||||
|
dx = (max_x - min_x) / self.grid_size
|
||||||
|
dy = (max_y - min_y) / self.grid_size
|
||||||
|
cx = min_x + dx * (gx + 0.5)
|
||||||
|
cy = min_y + dy * (gy + 0.5)
|
||||||
|
cz = (min_z + max_z) * 0.5
|
||||||
|
v = mvp_mat * QVector4D(cx, cy, cz, 1.0)
|
||||||
|
if v.w() == 0:
|
||||||
|
return False
|
||||||
|
ndc_x = v.x() / v.w()
|
||||||
|
ndc_y = v.y() / v.w()
|
||||||
|
ndc_z = v.z() / v.w()
|
||||||
|
return -1.5 <= ndc_x <= 1.5 and -1.5 <= ndc_y <= 1.5 and -2.0 <= ndc_z <= 2.0
|
||||||
|
|
||||||
|
def paintGL(self):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
|
if self.vertex_count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
actual_display_verts = min(self.vertex_count, self.progress_vertices)
|
||||||
|
|
||||||
|
if actual_display_verts > 500000:
|
||||||
|
desired_step = 4
|
||||||
|
elif actual_display_verts > 200000:
|
||||||
|
desired_step = 2
|
||||||
|
else:
|
||||||
|
desired_step = 1
|
||||||
|
|
||||||
|
if abs(self.view_zoom) > 600 and desired_step < 4:
|
||||||
|
desired_step = 4
|
||||||
|
elif abs(self.view_zoom) > 300 and desired_step < 2:
|
||||||
|
desired_step = 2
|
||||||
|
|
||||||
|
if desired_step not in self.vbos:
|
||||||
|
self._create_vbo_for_step(desired_step)
|
||||||
|
|
||||||
|
vbo_vertices, vbo_colors, vbo_count = self.vbos[desired_step]
|
||||||
|
if vbo_count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
scale = desired_step * 2
|
||||||
|
lod_progress = actual_display_verts // scale
|
||||||
|
if lod_progress > vbo_count:
|
||||||
|
lod_progress = vbo_count
|
||||||
|
|
||||||
|
self.shader_program.bind()
|
||||||
|
mvp_mat = self._build_mvp()
|
||||||
|
self.shader_program.setUniformValue(self.uMVP_location, mvp_mat)
|
||||||
|
|
||||||
|
vbo_vertices.bind()
|
||||||
|
self.shader_program.setAttributeBuffer(self.aPos_location, gl.GL_FLOAT, 0, 3, 0)
|
||||||
|
self.shader_program.enableAttributeArray(self.aPos_location)
|
||||||
|
|
||||||
|
vbo_colors.bind()
|
||||||
|
self.shader_program.setAttributeBuffer(self.aColor_location, gl.GL_FLOAT, 0, 3, 0)
|
||||||
|
self.shader_program.enableAttributeArray(self.aColor_location)
|
||||||
|
|
||||||
|
gl.glDepthFunc(gl.GL_LEQUAL)
|
||||||
|
|
||||||
|
use_culling = (desired_step == 1 and actual_display_verts > 100000 and self.grid_segments)
|
||||||
|
|
||||||
|
if use_culling:
|
||||||
|
# 空间剔除分支:现在每个格子条目是 (seg_idx, type_name)
|
||||||
|
for pass_idx in range(2):
|
||||||
|
if pass_idx == 0:
|
||||||
|
gl.glLineWidth(6.0)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 0.8)
|
||||||
|
else:
|
||||||
|
gl.glLineWidth(3.0)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 1.0)
|
||||||
|
|
||||||
|
for (gx, gy), seg_entries in self.grid_segments.items():
|
||||||
|
if not self._is_tile_visible(gx, gy, mvp_mat):
|
||||||
|
continue
|
||||||
|
for seg_idx, seg_type in seg_entries:
|
||||||
|
# 跳过不可见类型
|
||||||
|
if not self.type_visibility.get(seg_type, True):
|
||||||
|
continue
|
||||||
|
if seg_idx < actual_display_verts:
|
||||||
|
gl.glDrawArrays(gl.GL_LINES, seg_idx, 2)
|
||||||
|
else:
|
||||||
|
# 普通渲染分支
|
||||||
|
for pass_idx in range(2):
|
||||||
|
if pass_idx == 0:
|
||||||
|
gl.glLineWidth(6.0)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 0.8)
|
||||||
|
else:
|
||||||
|
gl.glLineWidth(3.0)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 1.0)
|
||||||
|
|
||||||
|
for type_name, segments in self.type_segments.items():
|
||||||
|
if not self.type_visibility.get(type_name, True):
|
||||||
|
continue
|
||||||
|
for start, length in segments:
|
||||||
|
lod_start = start // scale
|
||||||
|
lod_count = length // scale
|
||||||
|
if lod_start >= vbo_count:
|
||||||
|
continue
|
||||||
|
end = min(lod_start + lod_count, lod_progress)
|
||||||
|
actual_count = end - lod_start
|
||||||
|
if actual_count > 0:
|
||||||
|
gl.glDrawArrays(gl.GL_LINES, lod_start, actual_count)
|
||||||
|
|
||||||
|
gl.glDepthFunc(gl.GL_LESS)
|
||||||
|
self.shader_program.disableAttributeArray(self.aPos_location)
|
||||||
|
self.shader_program.disableAttributeArray(self.aColor_location)
|
||||||
|
vbo_vertices.release()
|
||||||
|
vbo_colors.release()
|
||||||
|
self.shader_program.release()
|
||||||
|
|
||||||
|
def _create_vbo_for_step(self, step):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
lod_data = self.lod_levels.get(step)
|
||||||
|
if not lod_data:
|
||||||
|
lod_data = self.lod_levels[1]
|
||||||
|
step = 1
|
||||||
|
|
||||||
|
verts = np.array(lod_data['points'], dtype=np.float32)
|
||||||
|
cols = np.array(lod_data['colors'], dtype=np.float32)
|
||||||
|
|
||||||
|
vbo_v = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||||
|
vbo_c = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||||
|
vbo_v.create()
|
||||||
|
vbo_v.bind()
|
||||||
|
vbo_v.allocate(verts.tobytes(), verts.nbytes)
|
||||||
|
vbo_v.release()
|
||||||
|
vbo_c.create()
|
||||||
|
vbo_c.bind()
|
||||||
|
vbo_c.allocate(cols.tobytes(), cols.nbytes)
|
||||||
|
vbo_c.release()
|
||||||
|
|
||||||
|
self.vbos[step] = (vbo_v, vbo_c, lod_data['count'])
|
||||||
|
|
||||||
|
# ── 交互部分保持不变 ──
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.last_mouse_pos = event.position()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self.last_mouse_pos is None:
|
||||||
|
return
|
||||||
|
dx = event.position().x() - self.last_mouse_pos.x()
|
||||||
|
dy = event.position().y() - self.last_mouse_pos.y()
|
||||||
|
if event.buttons() & Qt.MouseButton.LeftButton:
|
||||||
|
self.view_rot_x += dy * self._rot_sensitivity
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x))
|
||||||
|
self.view_rot_z += dx * self._rot_sensitivity
|
||||||
|
self.last_mouse_pos = event.position()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
delta = event.angleDelta().y() / 120
|
||||||
|
self.view_zoom += delta * 10
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def event(self, e):
|
||||||
|
if e.type() in (QEvent.Type.TouchBegin, QEvent.Type.TouchUpdate, QEvent.Type.TouchEnd):
|
||||||
|
self._ignore_wheel = True
|
||||||
|
self.touchEvent(e)
|
||||||
|
return True
|
||||||
|
elif e.type() == QEvent.Type.Wheel:
|
||||||
|
if self._ignore_wheel:
|
||||||
|
self._ignore_wheel = False
|
||||||
|
return True
|
||||||
|
return super().event(e)
|
||||||
|
|
||||||
|
def touchEvent(self, event: QTouchEvent):
|
||||||
|
points = event.points()
|
||||||
|
if not points:
|
||||||
|
return
|
||||||
|
if event.type() == QEvent.Type.TouchEnd:
|
||||||
|
self._touch_points.clear()
|
||||||
|
self._pinch_start_center = None
|
||||||
|
self._pinch_start_dist = 0.0
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
if len(points) == 1:
|
||||||
|
p = points[0]
|
||||||
|
if p.id() not in self._touch_points or self._pinch_start_center is not None:
|
||||||
|
self._touch_points.clear()
|
||||||
|
self._touch_points[p.id()] = p.position()
|
||||||
|
self._pinch_start_center = None
|
||||||
|
else:
|
||||||
|
last = self._touch_points[p.id()]
|
||||||
|
dx = p.position().x() - last.x()
|
||||||
|
dy = p.position().y() - last.y()
|
||||||
|
self.view_rot_x += dy * self._rot_sensitivity
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x))
|
||||||
|
self.view_rot_z += dx * self._rot_sensitivity
|
||||||
|
self._touch_points[p.id()] = p.position()
|
||||||
|
self.update()
|
||||||
|
elif len(points) == 2:
|
||||||
|
p1, p2 = points[0], points[1]
|
||||||
|
dx_p = p1.position().x() - p2.position().x()
|
||||||
|
dy_p = p1.position().y() - p2.position().y()
|
||||||
|
dist = math.hypot(dx_p, dy_p)
|
||||||
|
center_x = (p1.position().x() + p2.position().x()) / 2.0
|
||||||
|
center_y = (p1.position().y() + p2.position().y()) / 2.0
|
||||||
|
if self._pinch_start_center is None:
|
||||||
|
self._pinch_start_dist = dist
|
||||||
|
self._pinch_start_zoom = self.view_zoom
|
||||||
|
self._pinch_start_center = (center_x, center_y)
|
||||||
|
self._pinch_start_trans = (self.view_trans_x, self.view_trans_y)
|
||||||
|
self._touch_points.clear()
|
||||||
|
else:
|
||||||
|
if self._pinch_start_dist > 0:
|
||||||
|
scale = dist / self._pinch_start_dist
|
||||||
|
self.view_zoom = self._pinch_start_zoom * (1 / scale)
|
||||||
|
dcx = center_x - self._pinch_start_center[0]
|
||||||
|
dcy = center_y - self._pinch_start_center[1]
|
||||||
|
pan_speed = abs(self.view_zoom) * 0.002
|
||||||
|
self.view_trans_x = self._pinch_start_trans[0] + dcx * pan_speed
|
||||||
|
self.view_trans_y = self._pinch_start_trans[1] - dcy * pan_speed
|
||||||
|
self.update()
|
||||||
|
event.accept()
|
||||||
572
utils/gcode_viewer_old.py
Normal file
572
utils/gcode_viewer_old.py
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
import numpy as np
|
||||||
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||||
|
from PyQt6.QtCore import Qt, QEvent, QThread, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QTouchEvent, QSurfaceFormat
|
||||||
|
from PyQt6.QtOpenGL import QOpenGLShaderProgram, QOpenGLShader, QOpenGLBuffer
|
||||||
|
|
||||||
|
class GCodeParseWorker(QThread):
|
||||||
|
finished = pyqtSignal(dict)
|
||||||
|
|
||||||
|
def __init__(self, filepath, type_map, default_colors, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.filepath = filepath
|
||||||
|
self.TYPE_MAP = type_map
|
||||||
|
self.DEFAULT_COLORS = default_colors
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
points = []
|
||||||
|
colors = []
|
||||||
|
type_segments = {}
|
||||||
|
type_visibility = {}
|
||||||
|
|
||||||
|
x = y = z = e = 0.0
|
||||||
|
vertex_idx = 0
|
||||||
|
feature_type = 'OTHER'
|
||||||
|
current_segment_type = 'OTHER'
|
||||||
|
segment_start = 0
|
||||||
|
type_visibility['TRAVEL'] = False
|
||||||
|
relative_e = False
|
||||||
|
|
||||||
|
min_x = min_y = min_z = float('inf')
|
||||||
|
max_x = max_y = max_z = float('-inf')
|
||||||
|
|
||||||
|
current_offset = 0
|
||||||
|
max_z_seen = -999.0
|
||||||
|
layer_map = [(0, 0)]
|
||||||
|
|
||||||
|
def add_segment(t_name, start, length):
|
||||||
|
if length > 0:
|
||||||
|
type_segments.setdefault(t_name, []).append((start, length))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.filepath, 'rb') as f:
|
||||||
|
for line_bytes in f:
|
||||||
|
current_offset += len(line_bytes)
|
||||||
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.startswith('M82'):
|
||||||
|
relative_e = False
|
||||||
|
continue
|
||||||
|
if line.startswith('M83'):
|
||||||
|
relative_e = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(';'):
|
||||||
|
if line.startswith(';TYPE:'):
|
||||||
|
raw_type = line.split(':', 1)[1].strip()
|
||||||
|
else:
|
||||||
|
raw_type = line[1:].strip()
|
||||||
|
if raw_type not in self.TYPE_MAP and not any(k in raw_type.lower() for k in ('perimeter', 'infill', 'material', 'skirt/brim')):
|
||||||
|
continue
|
||||||
|
if 'Skirt/Brim' in raw_type:
|
||||||
|
raw_type = 'Skirt'
|
||||||
|
|
||||||
|
new_type = self.TYPE_MAP.get(raw_type, 'OTHER')
|
||||||
|
if new_type != feature_type:
|
||||||
|
feature_type = new_type
|
||||||
|
if feature_type not in type_visibility:
|
||||||
|
type_visibility[feature_type] = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(('G0', 'G1')):
|
||||||
|
new_x, new_y, new_z = x, y, z
|
||||||
|
e_val = 0.0
|
||||||
|
has_e = False
|
||||||
|
parts = line.split(';')[0].split()
|
||||||
|
for p in parts[1:]:
|
||||||
|
try:
|
||||||
|
if p.startswith('X'): new_x = float(p[1:])
|
||||||
|
elif p.startswith('Y'): new_y = float(p[1:])
|
||||||
|
elif p.startswith('Z'): new_z = float(p[1:])
|
||||||
|
elif p.startswith('E'):
|
||||||
|
e_val = float(p[1:])
|
||||||
|
has_e = True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if new_x == x and new_y == y and new_z == z:
|
||||||
|
if has_e:
|
||||||
|
if relative_e: e += e_val
|
||||||
|
else: e = e_val
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_extrusion = False
|
||||||
|
if has_e:
|
||||||
|
if relative_e:
|
||||||
|
is_extrusion = e_val > 0
|
||||||
|
new_e = e + e_val
|
||||||
|
else:
|
||||||
|
is_extrusion = e_val > e
|
||||||
|
new_e = e_val
|
||||||
|
else:
|
||||||
|
new_e = e
|
||||||
|
|
||||||
|
if is_extrusion:
|
||||||
|
seg_type = feature_type
|
||||||
|
c = self.DEFAULT_COLORS.get(feature_type, self.DEFAULT_COLORS['OTHER'])
|
||||||
|
else:
|
||||||
|
seg_type = 'TRAVEL'
|
||||||
|
c = self.DEFAULT_COLORS['TRAVEL']
|
||||||
|
if 'TRAVEL' not in type_visibility:
|
||||||
|
type_visibility['TRAVEL'] = False
|
||||||
|
|
||||||
|
if seg_type != current_segment_type:
|
||||||
|
if vertex_idx > segment_start:
|
||||||
|
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||||
|
current_segment_type = seg_type
|
||||||
|
segment_start = vertex_idx
|
||||||
|
|
||||||
|
if new_x < min_x: min_x = new_x
|
||||||
|
if new_x > max_x: max_x = new_x
|
||||||
|
if new_y < min_y: min_y = new_y
|
||||||
|
if new_y > max_y: max_y = new_y
|
||||||
|
if new_z < min_z: min_z = new_z
|
||||||
|
if new_z > max_z: max_z = new_z
|
||||||
|
|
||||||
|
if new_z > max_z_seen and is_extrusion:
|
||||||
|
max_z_seen = new_z
|
||||||
|
layer_map.append((current_offset, vertex_idx))
|
||||||
|
|
||||||
|
points.extend([x, y, z, new_x, new_y, new_z])
|
||||||
|
colors.extend([*c, *c])
|
||||||
|
vertex_idx += 2
|
||||||
|
x, y, z, e = new_x, new_y, new_z, new_e
|
||||||
|
elif line.startswith('G92'):
|
||||||
|
parts = line.split(';')[0].split()
|
||||||
|
for p in parts[1:]:
|
||||||
|
try:
|
||||||
|
if p.startswith('E'): e = float(p[1:])
|
||||||
|
elif p.startswith('X'): x = float(p[1:])
|
||||||
|
elif p.startswith('Y'): y = float(p[1:])
|
||||||
|
elif p.startswith('Z'): z = float(p[1:])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if vertex_idx > segment_start:
|
||||||
|
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||||
|
|
||||||
|
cx = (min_x + max_x) / 2.0 if max_x >= min_x else 110.0
|
||||||
|
cy = (min_y + max_y) / 2.0 if max_y >= min_y else 110.0
|
||||||
|
cz = (min_z + max_z) / 2.0 if max_z >= min_z else 0.0
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'vertices': np.array(points, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||||
|
'colors': np.array(colors, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||||
|
'vertex_count': vertex_idx,
|
||||||
|
'center_x': cx,
|
||||||
|
'center_y': cy,
|
||||||
|
'center_z': cz,
|
||||||
|
'type_segments': type_segments,
|
||||||
|
'type_visibility': type_visibility,
|
||||||
|
'layer_map': layer_map
|
||||||
|
}
|
||||||
|
self.finished.emit(result)
|
||||||
|
except Exception as e:
|
||||||
|
print("ParseGCode Error:", e)
|
||||||
|
self.finished.emit({})
|
||||||
|
|
||||||
|
class GCodeViewerWidget(QOpenGLWidget):
|
||||||
|
"""
|
||||||
|
3D G-code 预览控件(OpenGL ES / eglfs 兼容版)
|
||||||
|
使用可编程管线替代固定管线,适配树莓派 4B
|
||||||
|
"""
|
||||||
|
|
||||||
|
# PrusaSlicer 类型映射、颜色定义等保持不变
|
||||||
|
TYPE_MAP = {
|
||||||
|
'External perimeter': 'WALL-OUTER',
|
||||||
|
'Perimeter': 'WALL-INNER',
|
||||||
|
'Overhang perimeter': 'WALL-OUTER',
|
||||||
|
'Solid infill': 'SKIN',
|
||||||
|
'Top solid infill': 'SKIN',
|
||||||
|
'Bridge infill': 'SKIN',
|
||||||
|
'Internal infill': 'FILL',
|
||||||
|
'Support material': 'SUPPORT',
|
||||||
|
'Support material interface': 'SUPPORT-INTERFACE',
|
||||||
|
'Skirt': 'SKIRT',
|
||||||
|
'Brim': 'SKIRT',
|
||||||
|
'Custom': 'OTHER',
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_COLORS = {
|
||||||
|
'WALL-OUTER': (0.92, 0.55, 0.22), # 0xeb8b38
|
||||||
|
'WALL-INNER': (0.25, 0.50, 0.81), # 0x4080cf
|
||||||
|
'FILL': (0.80, 0.75, 0.29), # 0xccc04b
|
||||||
|
'SKIN': (0.62, 0.38, 0.70), # 0x9e60b3
|
||||||
|
'SUPPORT': (0.34, 0.70, 0.34), # 0x57b357
|
||||||
|
'SUPPORT-INTERFACE': (0.17, 0.42, 0.17), # 0x2b6b2b
|
||||||
|
'SKIRT': (0.00, 1.00, 1.00), # 0x00ffff
|
||||||
|
'OTHER': (0.67, 0.67, 0.67), # 0xaaaaaa
|
||||||
|
'TRAVEL': (0.25, 0.31, 0.38), # 0x405060
|
||||||
|
}
|
||||||
|
|
||||||
|
# 顶点着色器(GLSL ES 1.00)
|
||||||
|
VERTEX_SHADER = """
|
||||||
|
attribute vec3 aPos;
|
||||||
|
attribute vec3 aColor;
|
||||||
|
varying vec3 vColor;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
void main() {
|
||||||
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||||
|
vColor = aColor;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 片段着色器(添加可调节颜色深度的 uniform 以实现边界加深)
|
||||||
|
FRAGMENT_SHADER = """
|
||||||
|
precision mediump float;
|
||||||
|
varying vec3 vColor;
|
||||||
|
uniform float uDarken;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(vColor * uDarken, 1.0);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
# 请求 OpenGL ES 2.0 上下文
|
||||||
|
fmt = QSurfaceFormat()
|
||||||
|
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGLES)
|
||||||
|
fmt.setVersion(2, 0)
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumSize(400, 300)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True)
|
||||||
|
self.setFormat(fmt)
|
||||||
|
# 数据缓冲区
|
||||||
|
self.vertices = None
|
||||||
|
self.colors = None
|
||||||
|
self.vertex_count = 0
|
||||||
|
self.vbo_vertices = None
|
||||||
|
self.vbo_colors = None
|
||||||
|
self.vbo_ready = False
|
||||||
|
|
||||||
|
# 类型分段
|
||||||
|
self.type_segments = {}
|
||||||
|
self.type_visibility = {}
|
||||||
|
self.current_type = 'OTHER'
|
||||||
|
|
||||||
|
# 视角参数
|
||||||
|
self.view_rot_x = -60.0
|
||||||
|
self.view_rot_z = 45.0
|
||||||
|
self.view_zoom = -250.0 # 稍微退后一点以便看到整个打印平台
|
||||||
|
self.view_trans_x = 0.0
|
||||||
|
self.view_trans_y = 0.0
|
||||||
|
|
||||||
|
# ...
|
||||||
|
self.progress_ratio = 1.0
|
||||||
|
self.progress_vertices = 0
|
||||||
|
|
||||||
|
# 按需渲染:缓存当前的 filepos 对应的顶点数
|
||||||
|
self.layer_map = [(0, 0)] # (offset_bytes, vertex_idx)
|
||||||
|
self.last_reported_filepos = -1
|
||||||
|
|
||||||
|
# 模型中心点
|
||||||
|
self.center_x = 110.0
|
||||||
|
self.center_y = 110.0
|
||||||
|
self.center_z = 0.0
|
||||||
|
|
||||||
|
# 触摸状态
|
||||||
|
self.last_mouse_pos = None
|
||||||
|
self._touch_points = {}
|
||||||
|
self._pinch_start_dist = 0.0
|
||||||
|
self._pinch_start_zoom = 0.0
|
||||||
|
self._pinch_start_center = None
|
||||||
|
self._pinch_start_trans = (0.0, 0.0)
|
||||||
|
self._ignore_wheel = False
|
||||||
|
self._rot_sensitivity = 0.1
|
||||||
|
|
||||||
|
# 着色器程序
|
||||||
|
self.shader_program = None
|
||||||
|
self.aPos_location = None
|
||||||
|
self.aColor_location = None
|
||||||
|
self.uMVP_location = None
|
||||||
|
|
||||||
|
# ── 公开接口 ──
|
||||||
|
def load_gcode(self, filepath: str):
|
||||||
|
if hasattr(self, '_worker') and self._worker.isRunning():
|
||||||
|
self._worker.terminate()
|
||||||
|
self._worker.wait()
|
||||||
|
|
||||||
|
self._worker = GCodeParseWorker(filepath, self.TYPE_MAP, self.DEFAULT_COLORS)
|
||||||
|
self._worker.finished.connect(self._on_parse_finished)
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
|
def _on_parse_finished(self, result: dict):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.vertices = result['vertices']
|
||||||
|
self.colors = result['colors']
|
||||||
|
self.vertex_count = result['vertex_count']
|
||||||
|
self.center_x = result['center_x']
|
||||||
|
self.center_y = result['center_y']
|
||||||
|
self.center_z = result['center_z']
|
||||||
|
self.type_segments = result['type_segments']
|
||||||
|
self.type_visibility = result['type_visibility']
|
||||||
|
self.layer_map = result['layer_map']
|
||||||
|
|
||||||
|
self.vbo_ready = False
|
||||||
|
# 初始时先不显示,让 update_by_filepos 决定,或者如果是0则自动更新
|
||||||
|
# 这里把 progress_vertices 赋予当前的 target
|
||||||
|
target = 0
|
||||||
|
if self.layer_map:
|
||||||
|
target = self.layer_map[-1][1] # 全显
|
||||||
|
|
||||||
|
self.progress_vertices = target
|
||||||
|
self.last_reported_filepos = -1
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update_processes(self, progress: float):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_by_filepos(self, filepos: int, is_printing: bool = True):
|
||||||
|
import bisect
|
||||||
|
if not hasattr(self, 'layer_map') or not self.layer_map:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_printing:
|
||||||
|
# 不在打印途中时,渲染包含所有顶点(完整模型)
|
||||||
|
target_vertices = self.vertex_count
|
||||||
|
else:
|
||||||
|
# 使用二分查找快速定位当前 filepos 对应的最多展示顶点数
|
||||||
|
keys = [item[0] for item in self.layer_map]
|
||||||
|
idx = bisect.bisect_right(keys, filepos)
|
||||||
|
target_vertices = 0 if idx == 0 else self.layer_map[idx-1][1]
|
||||||
|
|
||||||
|
# 按需渲染:只有当层级(计算出的可见顶点数)真正发生跳变时才调用 update()
|
||||||
|
if target_vertices != getattr(self, 'progress_vertices', -1):
|
||||||
|
self.progress_vertices = target_vertices
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update_switch(self, type_name: str, visible: bool):
|
||||||
|
if type_name in self.type_visibility:
|
||||||
|
self.type_visibility[type_name] = visible
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def set_view_angles(self, rot_x: float, rot_z: float, zoom: float = None):
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, rot_x))
|
||||||
|
self.view_rot_z = rot_z
|
||||||
|
if zoom is not None:
|
||||||
|
self.view_zoom = zoom
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
if length == 0:
|
||||||
|
return
|
||||||
|
self.type_segments.setdefault(type_name, []).append((start, length))
|
||||||
|
|
||||||
|
# ── OpenGL 可编程管线初始化 ──
|
||||||
|
def initializeGL(self):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glClearColor(0.15, 0.15, 0.15, 1.0)
|
||||||
|
gl.glEnable(gl.GL_DEPTH_TEST)
|
||||||
|
gl.glLineWidth(1.0)
|
||||||
|
|
||||||
|
# 编译着色器
|
||||||
|
self.shader_program = QOpenGLShaderProgram()
|
||||||
|
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, self.VERTEX_SHADER)
|
||||||
|
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, self.FRAGMENT_SHADER)
|
||||||
|
self.shader_program.link()
|
||||||
|
|
||||||
|
# 获取属性和 uniform 位置
|
||||||
|
self.aPos_location = self.shader_program.attributeLocation("aPos")
|
||||||
|
self.aColor_location = self.shader_program.attributeLocation("aColor")
|
||||||
|
self.uMVP_location = self.shader_program.uniformLocation("uMVP")
|
||||||
|
self.uDarken_location = self.shader_program.uniformLocation("uDarken")
|
||||||
|
|
||||||
|
# 创建缓冲对象
|
||||||
|
self.vbo_vertices = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||||
|
self.vbo_colors = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||||
|
|
||||||
|
def resizeGL(self, w, h):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glViewport(0, 0, w, h)
|
||||||
|
|
||||||
|
# ── 构建 MVP 矩阵(替代 glOrtho/glRotate) ──
|
||||||
|
def _build_mvp(self):
|
||||||
|
from PyQt6.QtGui import QMatrix4x4
|
||||||
|
mat = QMatrix4x4()
|
||||||
|
aspect = self.width() / self.height() if self.height() else 1
|
||||||
|
mat.perspective(45.0, aspect, 1.0, 1000.0)
|
||||||
|
|
||||||
|
mat.translate(self.view_trans_x, self.view_trans_y, self.view_zoom)
|
||||||
|
mat.rotate(self.view_rot_x, 1.0, 0.0, 0.0)
|
||||||
|
mat.rotate(self.view_rot_z, 0.0, 0.0, 1.0)
|
||||||
|
mat.translate(-self.center_x, -self.center_y, -self.center_z)
|
||||||
|
|
||||||
|
return mat
|
||||||
|
|
||||||
|
# ── 渲染 ──
|
||||||
|
def paintGL(self):
|
||||||
|
import OpenGL.GL as gl
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
|
if self.vertices is None or self.vertex_count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.vbo_ready:
|
||||||
|
self._create_vbos()
|
||||||
|
self.vbo_ready = True
|
||||||
|
|
||||||
|
# 使用着色器程序
|
||||||
|
self.shader_program.bind()
|
||||||
|
|
||||||
|
# 设置 MVP 矩阵
|
||||||
|
import math
|
||||||
|
from PyQt6.QtGui import QMatrix4x4
|
||||||
|
mvp_mat = self._build_mvp()
|
||||||
|
self.shader_program.setUniformValue(self.uMVP_location, mvp_mat)
|
||||||
|
|
||||||
|
# 绑定 VBO
|
||||||
|
self.vbo_vertices.bind()
|
||||||
|
self.shader_program.setAttributeBuffer(self.aPos_location, gl.GL_FLOAT, 0, 3, 0)
|
||||||
|
self.shader_program.enableAttributeArray(self.aPos_location)
|
||||||
|
|
||||||
|
self.vbo_colors.bind()
|
||||||
|
self.shader_program.setAttributeBuffer(self.aColor_location, gl.GL_FLOAT, 0, 3, 0)
|
||||||
|
self.shader_program.enableAttributeArray(self.aColor_location)
|
||||||
|
|
||||||
|
# 允许 z-fighting 覆盖,用于同一位置多次渲染线条
|
||||||
|
gl.glDepthFunc(gl.GL_LEQUAL)
|
||||||
|
|
||||||
|
# 渲染两次:一次绘制加粗加深的边界底线,一次绘制正常宽度的原色骨架线
|
||||||
|
# (在树莓派等性能有限的平台上,使用真实的3D圆柱/方块代替线条会导致顶点数暴增十几倍直接卡顿,
|
||||||
|
# 因此通过动态加粗像素级线宽来性能无损地模拟出“体积感”)
|
||||||
|
for pass_idx in range(2):
|
||||||
|
if pass_idx == 0:
|
||||||
|
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 0.8) # 加深颜色至 40% 亮度
|
||||||
|
else:
|
||||||
|
gl.glLineWidth(3.0) # 主体宽度(加大以模拟线条厚度)
|
||||||
|
self.shader_program.setUniformValue(self.uDarken_location, 1.0) # 保持原色
|
||||||
|
|
||||||
|
# 按类型分段绘制
|
||||||
|
for type_name, segments in self.type_segments.items():
|
||||||
|
if not self.type_visibility.get(type_name, True):
|
||||||
|
continue
|
||||||
|
for start, length in segments:
|
||||||
|
if start >= self.progress_vertices:
|
||||||
|
continue
|
||||||
|
end = start + length
|
||||||
|
visible_start = start
|
||||||
|
visible_count = length
|
||||||
|
if end > self.progress_vertices:
|
||||||
|
visible_count = self.progress_vertices - start
|
||||||
|
if visible_count > 0:
|
||||||
|
gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)
|
||||||
|
|
||||||
|
# 恢复默认深度测试模式
|
||||||
|
gl.glDepthFunc(gl.GL_LESS)
|
||||||
|
|
||||||
|
self.shader_program.disableAttributeArray(self.aPos_location)
|
||||||
|
self.shader_program.disableAttributeArray(self.aColor_location)
|
||||||
|
self.vbo_vertices.release()
|
||||||
|
self.vbo_colors.release()
|
||||||
|
self.shader_program.release()
|
||||||
|
|
||||||
|
def _create_vbos(self):
|
||||||
|
if self.vbo_vertices.isCreated():
|
||||||
|
self.vbo_vertices.destroy()
|
||||||
|
if self.vbo_colors.isCreated():
|
||||||
|
self.vbo_colors.destroy()
|
||||||
|
|
||||||
|
self.vbo_vertices.create()
|
||||||
|
self.vbo_vertices.bind()
|
||||||
|
self.vbo_vertices.allocate(self.vertices.tobytes(), self.vertices.nbytes)
|
||||||
|
self.vbo_vertices.release()
|
||||||
|
|
||||||
|
self.vbo_colors.create()
|
||||||
|
self.vbo_colors.bind()
|
||||||
|
self.vbo_colors.allocate(self.colors.tobytes(), self.colors.nbytes)
|
||||||
|
self.vbo_colors.release()
|
||||||
|
|
||||||
|
# ── 触摸/鼠标交互(完全不变) ──
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.last_mouse_pos = event.position()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self.last_mouse_pos is None:
|
||||||
|
return
|
||||||
|
dx = event.position().x() - self.last_mouse_pos.x()
|
||||||
|
dy = event.position().y() - self.last_mouse_pos.y()
|
||||||
|
if event.buttons() & Qt.MouseButton.LeftButton:
|
||||||
|
self.view_rot_x += dy * self._rot_sensitivity
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x)) # 限制垂直视角的翻转
|
||||||
|
self.view_rot_z += dx * self._rot_sensitivity
|
||||||
|
self.last_mouse_pos = event.position()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
delta = event.angleDelta().y() / 120
|
||||||
|
self.view_zoom += delta * 10
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def event(self, e):
|
||||||
|
if e.type() in (QEvent.Type.TouchBegin, QEvent.Type.TouchUpdate, QEvent.Type.TouchEnd):
|
||||||
|
self._ignore_wheel = True
|
||||||
|
self.touchEvent(e)
|
||||||
|
return True
|
||||||
|
elif e.type() == QEvent.Type.Wheel:
|
||||||
|
if self._ignore_wheel:
|
||||||
|
self._ignore_wheel = False
|
||||||
|
return True
|
||||||
|
return super().event(e)
|
||||||
|
|
||||||
|
def touchEvent(self, event: QTouchEvent):
|
||||||
|
points = event.points()
|
||||||
|
if not points:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.type() == QEvent.Type.TouchEnd:
|
||||||
|
self._touch_points.clear()
|
||||||
|
self._pinch_start_center = None
|
||||||
|
self._pinch_start_dist = 0.0
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(points) == 1:
|
||||||
|
p = points[0]
|
||||||
|
# 如果是刚检测到单指,或者从双指变回单指
|
||||||
|
if p.id() not in self._touch_points or self._pinch_start_center is not None:
|
||||||
|
self._touch_points.clear()
|
||||||
|
self._touch_points[p.id()] = p.position()
|
||||||
|
self._pinch_start_center = None
|
||||||
|
else:
|
||||||
|
last = self._touch_points[p.id()]
|
||||||
|
dx = p.position().x() - last.x()
|
||||||
|
dy = p.position().y() - last.y()
|
||||||
|
self.view_rot_x += dy * self._rot_sensitivity
|
||||||
|
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x)) # 限制垂直视角的翻转
|
||||||
|
self.view_rot_z += dx * self._rot_sensitivity
|
||||||
|
self._touch_points[p.id()] = p.position()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
elif len(points) == 2:
|
||||||
|
p1, p2 = points[0], points[1]
|
||||||
|
dx_p = p1.position().x() - p2.position().x()
|
||||||
|
dy_p = p1.position().y() - p2.position().y()
|
||||||
|
dist = (dx_p**2 + dy_p**2)**0.5
|
||||||
|
center_x = (p1.position().x() + p2.position().x()) / 2.0
|
||||||
|
center_y = (p1.position().y() + p2.position().y()) / 2.0
|
||||||
|
|
||||||
|
# 初始化双指状态
|
||||||
|
if self._pinch_start_center is None:
|
||||||
|
self._pinch_start_dist = dist
|
||||||
|
self._pinch_start_zoom = self.view_zoom
|
||||||
|
self._pinch_start_center = (center_x, center_y)
|
||||||
|
self._pinch_start_trans = (self.view_trans_x, self.view_trans_y)
|
||||||
|
self._touch_points.clear() # 清除单指记录
|
||||||
|
else:
|
||||||
|
# 缩放 (双指捏合)
|
||||||
|
if self._pinch_start_dist > 0:
|
||||||
|
scale = dist / self._pinch_start_dist
|
||||||
|
self.view_zoom = self._pinch_start_zoom * (1 / scale)
|
||||||
|
|
||||||
|
# 平移 (双指并行移动)
|
||||||
|
dcx = center_x - self._pinch_start_center[0]
|
||||||
|
dcy = center_y - self._pinch_start_center[1]
|
||||||
|
pan_speed = abs(self.view_zoom) * 0.002
|
||||||
|
self.view_trans_x = self._pinch_start_trans[0] + dcx * pan_speed
|
||||||
|
self.view_trans_y = self._pinch_start_trans[1] - dcy * pan_speed
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
event.accept()
|
||||||
87
utils/modern_confirm.py
Normal file
87
utils/modern_confirm.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget
|
||||||
|
from PyQt6.QtCore import Qt, QSize
|
||||||
|
from PyQt6.QtGui import QPixmap
|
||||||
|
from .get_bootstrap_icon import get_colored_pixmap, get_colored_icon
|
||||||
|
|
||||||
|
|
||||||
|
class ModernConfirmDialog(QDialog):
|
||||||
|
"""符合项目深色圆角主题的现代确认对话框
|
||||||
|
|
||||||
|
用法:dlg = ModernConfirmDialog(parent, title, message, icon_name)
|
||||||
|
if dlg.exec() == QDialog.DialogCode.Accepted: ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget, title: str, message: str, icon_name: str = "question-circle.svg",
|
||||||
|
confirm_text: str = "确定", cancel_text: str = "取消"):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setModal(True)
|
||||||
|
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
|
||||||
|
self.setFixedWidth(560)
|
||||||
|
|
||||||
|
# 样式
|
||||||
|
self.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QDialog { background-color: #3f3f3f; border: 2px solid #646464; border-radius: 12px; }
|
||||||
|
QLabel#title { color: #f2f2f2; font-size: 22px; font-weight: 700; background-color: transparent;}
|
||||||
|
QLabel#message { color: #dcdcdc; font-size: 18px; background-color: transparent;}
|
||||||
|
QPushButton#confirm { min-width: 140px; min-height: 48px; font-size: 18px; font-weight: 700; color: #ffffff; background-color: #2f6f91; border: 2px solid #4a9fc8; border-radius: 10px; }
|
||||||
|
QPushButton#confirm:hover { background-color: #3a85b3; }
|
||||||
|
QPushButton#cancel { min-width: 120px; min-height: 48px; font-size: 18px; font-weight: 600; color: #f2f2f2; background-color: #555555; border: 2px solid #707070; border-radius: 10px; }
|
||||||
|
QPushButton#cancel:hover { background-color: #636363; }
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(20, 20, 20, 20)
|
||||||
|
root.setSpacing(16)
|
||||||
|
|
||||||
|
top_row = QHBoxLayout()
|
||||||
|
top_row.setSpacing(16)
|
||||||
|
|
||||||
|
# 图标
|
||||||
|
icon_label = QLabel()
|
||||||
|
icon_label.setStyleSheet("background-color: transparent;")
|
||||||
|
icon_pix = get_colored_pixmap(icon_name, "#4a9fc8", 64, 64)
|
||||||
|
if isinstance(icon_pix, QPixmap):
|
||||||
|
icon_label.setPixmap(icon_pix.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
icon_label.setFixedSize(72, 72)
|
||||||
|
top_row.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
# 文本列
|
||||||
|
text_col = QVBoxLayout()
|
||||||
|
title_lbl = QLabel(title)
|
||||||
|
title_lbl.setObjectName("title")
|
||||||
|
text_col.addWidget(title_lbl)
|
||||||
|
|
||||||
|
msg_lbl = QLabel(message)
|
||||||
|
msg_lbl.setObjectName("message")
|
||||||
|
msg_lbl.setWordWrap(True)
|
||||||
|
text_col.addWidget(msg_lbl)
|
||||||
|
|
||||||
|
top_row.addLayout(text_col)
|
||||||
|
root.addLayout(top_row)
|
||||||
|
|
||||||
|
# 按钮行
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
btn_row.addStretch()
|
||||||
|
|
||||||
|
cancel_btn = QPushButton(cancel_text)
|
||||||
|
cancel_btn.setObjectName("cancel")
|
||||||
|
cancel_icon = get_colored_icon("x-circle.svg", "#f2f2f2", 20, 20)
|
||||||
|
if cancel_icon:
|
||||||
|
cancel_btn.setIcon(cancel_icon)
|
||||||
|
cancel_btn.setIconSize(QSize(18, 18))
|
||||||
|
cancel_btn.clicked.connect(self.reject)
|
||||||
|
btn_row.addWidget(cancel_btn)
|
||||||
|
|
||||||
|
confirm_btn = QPushButton(confirm_text)
|
||||||
|
confirm_btn.setObjectName("confirm")
|
||||||
|
confirm_icon = get_colored_icon("check-circle.svg", "#ffffff", 20, 20)
|
||||||
|
if confirm_icon:
|
||||||
|
confirm_btn.setIcon(confirm_icon)
|
||||||
|
confirm_btn.setIconSize(QSize(18, 18))
|
||||||
|
confirm_btn.clicked.connect(self.accept)
|
||||||
|
btn_row.addWidget(confirm_btn)
|
||||||
|
|
||||||
|
root.addLayout(btn_row)
|
||||||
Reference in New Issue
Block a user