优化ui,正在优化gcode_viewer

This commit is contained in:
2026-05-16 23:21:20 +08:00
parent d80e8dd05d
commit 1c0fc59738
11 changed files with 1480 additions and 99 deletions

View File

@@ -13,5 +13,18 @@
"move_max_speed": {
"xy": 3000,
"z": 200
},
"home_positions": {
"x": 134,
"y": 123,
"z": 10
},
"hotend_temp_range": {
"min": 0,
"max": 300
},
"bed_temp_range": {
"min": 0,
"max": 50
}
}

View File

@@ -63,9 +63,17 @@ echo "激活虚拟环境并升级 pip..."
source "$VENVDIR/bin/activate"
python -m pip install --upgrade pip setuptools wheel
PYTHON_VERSION=$(ls $VENVDIR/lib/ | grep python3)
if [ "${install_pyqt_via_pip:-0}" = "1" ]; then
echo "在 venv 中通过 pip 安装 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
REQ_FILE="$SCRIPT_DIR/requirements.txt"

View File

@@ -275,7 +275,6 @@ class ControlPage(QWidget):
def _on_config_changed(self, config_instance):
self._load_limits()
self._load_speeds()
print(f"z-max-s:{self.move_speed_z}")
# ── 状态管理 ──────────────────────────────────────────

View File

@@ -8,6 +8,7 @@ from PyQt6.QtWidgets import (
QPushButton,
QLineEdit,
QMessageBox,
QDialog,
QFormLayout,
QComboBox,
QListWidgetItem,
@@ -28,6 +29,7 @@ import re
from utils.wifi_manager import WifiManager
from utils.floating_keyboard import FloatingKeyboard
from utils.get_bootstrap_icon import get_colored_icon, get_colored_pixmap
from utils.modern_confirm import ModernConfirmDialog
class DragScrollArea(QScrollArea):
@@ -344,7 +346,7 @@ class SettingPage(QWidget):
color: #f2f2f2;
font-size: 30px;
padding: 10px;
background-color: #3f3f3f;
background-color: transparent;
}
QMessageBox QPushButton {
min-width: 130px;
@@ -1016,32 +1018,25 @@ class SettingPage(QWidget):
self.settings_stack.addWidget(self._wrap_scroll(power_widget))
def _confirm_reboot(self):
reply = QMessageBox.question(
self, "确认重启",
"确定要重启系统吗?\n所有未保存的数据将丢失。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
dlg = ModernConfirmDialog(self, "确认重启", "确定要重启系统吗?\n所有未保存的数据将丢失。", "question-circle.svg", confirm_text="重启", cancel_text="取消")
if dlg.exec() == QDialog.DialogCode.Accepted:
# QTimer.singleShot(500, lambda: print("test sudo reboot"))
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
self._styled_message(
"info", self, "重启",
"系统正在重启..."
)
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
def _confirm_shutdown(self):
reply = QMessageBox.question(
self, "确认关机",
"确定要关闭系统吗?\n关闭后需要手动重新开机。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
dlg = ModernConfirmDialog(self, "确认关机", "确定要关闭系统吗?\n关闭后需要手动重新开机。", "question-circle.svg", confirm_text="关机", cancel_text="取消")
if dlg.exec() == QDialog.DialogCode.Accepted:
# QTimer.singleShot(500, lambda: print("test sudo poweroff"))
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
self._styled_message(
"info", self, "关机",
"系统正在关机..."
)
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
def init_todo_settings(self):
todo_widget = QWidget()

View File

@@ -71,14 +71,19 @@ class CardFrame(QFrame):
class TempGauge(QWidget):
"""温度计指示器"""
def __init__(self, label="Tool", parent=None):
def __init__(self, label="Tool", temp_range=(0, 300), parent=None):
super().__init__(parent)
self.setFixedSize(100, 80)
self.setFixedSize(160, 90)
self._label = label
self._actual = 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._target = target
self.update()
@@ -89,14 +94,14 @@ class TempGauge(QWidget):
w, h = self.width(), self.height()
# 背景条
bar_x, bar_w = 16, 20
bar_x, bar_w = 60, 20
bar_y, bar_h = 10, 56
p.setPen(QPen(QColor("#555555"), 1))
p.setBrush(QBrush(QColor("#2a2a2a")))
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
# 填充柱(按温度比例,最高 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)
if fill_h > 0:
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
@@ -108,28 +113,29 @@ class TempGauge(QWidget):
# 目标值标记线
if self._target > 0:
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
p.setPen(QPen(QColor("#ffffff"), 2))
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("#888888"), 2))
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
# 文字
font = QFont("sans-serif", 11, QFont.Weight.Bold)
p.setFont(font)
p.setPen(QPen(QColor("#e0e0e0")))
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._actual:.0f}°")
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.setPen(QPen(QColor(temp_to_hex_soft(self._actual))))
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:
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)
p.setFont(font2)
p.setPen(QPen(QColor("#888888")))
p.drawText(44, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._target:.0f}°")
p.drawText(90, tgt_y - 10, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._target:.1f}°C")
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
p.setFont(font3)
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.config_changed.connect(self._on_config_changed)
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.init_ui()
@@ -165,6 +173,8 @@ class StatusPage(QWidget):
def _on_config_changed(self, config_instance):
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):
data = self.api_client.get_status()
@@ -272,8 +282,8 @@ class StatusPage(QWidget):
self._temp_card = CardFrame("温度")
temp_row = QHBoxLayout()
temp_row.setSpacing(8)
self._tool_gauge = TempGauge("喷头")
self._bed_gauge = TempGauge("热床")
self._tool_gauge = TempGauge("喷头", self.tool_temp_range)
self._bed_gauge = TempGauge("热床", self.bed_temp_range)
temp_row.addWidget(self._tool_gauge)
temp_row.addWidget(self._bed_gauge)
temp_row.addStretch()
@@ -361,8 +371,8 @@ class StatusPage(QWidget):
""")
# 温度
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target)
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_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_temp_range)
# G-code 模型加载与进度更新
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")
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

View File

@@ -1,4 +1,5 @@
import requests
import random
class AIOPrrintSystemAPI:
def __init__(self, api_url="http://127.0.0.1:5001/api/v1", api_key=""):
@@ -22,18 +23,18 @@ class AIOPrrintSystemAPI:
# 'job': {
# 'estimatedPrintTime': 1234,
# '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,
# 'user': None
# },
# 'progress': {
# 'completion': 74.8,
# 'filepos': 1234,
# 'printTime': 1235,
# 'printTimeLeft': 6353,
# 'completion': random.uniform(0, 100.0),
# 'filepos': 20934490,
# 'printTime': random.randint(1234,54321),
# 'printTimeLeft': random.randint(1234,54321),
# 'printTimeLeftOrigin': 5366
# },
# 'state': 'Operational'
# 'state': 'Printing'
# },
# 'status': {
# 'sd': {'ready': False},
@@ -44,19 +45,19 @@ class AIOPrrintSystemAPI:
# 'closedOrError': False,
# 'error': False,
# 'finishing': False,
# 'operational': True,
# 'operational': False,
# 'paused': False,
# 'pausing': False,
# 'printing': False,
# 'printing': True,
# 'ready': True,
# 'resuming': False,
# 'sdReady': False
# },
# 'text': 'Operational test'
# 'text': 'Printing'
# },
# 'temperature': {
# 'bed': {'actual': 85, 'offset': 0, 'target': 56},
# 'tool0': {'actual': 0.0, 'offset': 0, 'target': 340}
# 'bed': {'actual': random.randint(0,100), 'offset': 0, 'target': random.randint(0,100)},
# 'tool0': {'actual': random.randint(0,300), 'offset': 0, 'target': random.randint(0,300)}
# }
# }
# }

View File

@@ -15,6 +15,8 @@ class ConfigParse(QObject):
self.gcode_dir = None
self.move_axis_area = None
self.move_max_speed = None
self.hotend_temp_range = None
self.bed_temp_range = None
self.config = self._load_config()
self._parse_config()
@@ -67,4 +69,6 @@ class ConfigParse(QObject):
self.move_axis_area = self.config.get("move_axis_area", None)
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.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})

View File

@@ -17,6 +17,7 @@ class GCodeParseWorker(QThread):
points = []
colors = []
type_segments = {}
segment_zs = {}
type_visibility = {}
x = y = z = e = 0.0
@@ -25,6 +26,7 @@ class GCodeParseWorker(QThread):
current_segment_type = 'OTHER'
segment_start = 0
type_visibility['TRAVEL'] = False
relative_e = False
min_x = min_y = min_z = float('inf')
@@ -32,11 +34,16 @@ class GCodeParseWorker(QThread):
current_offset = 0
max_z_seen = -999.0
z_deltas = []
last_layer_z = None
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:
type_segments.setdefault(t_name, []).append((start, length))
segment_zs[(start, length)] = z_val
try:
with open(self.filepath, 'rb') as f:
@@ -113,7 +120,10 @@ class GCodeParseWorker(QThread):
if seg_type != current_segment_type:
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
segment_start = vertex_idx
@@ -127,6 +137,18 @@ class GCodeParseWorker(QThread):
if new_z > max_z_seen and is_extrusion:
max_z_seen = new_z
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])
colors.extend([*c, *c])
@@ -144,12 +166,47 @@ class GCodeParseWorker(QThread):
pass
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
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
from collections import Counter
layer_height = 0.2
if z_deltas:
counter = Counter(z_deltas)
layer_height = counter.most_common(1)[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),
@@ -159,7 +216,10 @@ class GCodeParseWorker(QThread):
'center_z': cz,
'type_segments': type_segments,
'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)
except Exception as e:
@@ -281,6 +341,12 @@ class GCodeViewerWidget(QOpenGLWidget):
self.aColor_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):
if hasattr(self, '_worker') and self._worker.isRunning():
@@ -302,8 +368,13 @@ class GCodeViewerWidget(QOpenGLWidget):
self.center_y = result['center_y']
self.center_z = result['center_z']
self.type_segments = result['type_segments']
self.segment_zs = result['segment_zs']
self.type_visibility = result['type_visibility']
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
# 初始时先不显示,让 update_by_filepos 决定或者如果是0则自动更新
@@ -311,6 +382,7 @@ class GCodeViewerWidget(QOpenGLWidget):
target = 0
if self.layer_map:
target = self.layer_map[-1][1] # 全显
self.layer_height = result.get('layer_height', 0.2)
self.progress_vertices = target
self.last_reported_filepos = -1
@@ -433,24 +505,88 @@ class GCodeViewerWidget(QOpenGLWidget):
# 因此通过动态加粗像素级线宽来性能无损地模拟出“体积感”)
for pass_idx in range(2):
if pass_idx == 0:
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
if self.enable_lod:
gl.glLineWidth(3.0) # 底线宽度(加大以模拟体积轮廓)
else:
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
self.shader_program.setUniformValue(self.uDarken_location, 0.8) # 加深颜色至 40% 亮度
else:
gl.glLineWidth(3.0) # 主体宽度(加大以模拟线条厚度)
if self.enable_lod:
gl.glLineWidth(1.5) # 主体宽度(加大以模拟线条厚度)
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)
# 按类型分段绘制带LOD
for type_name, segments in self.type_segments.items():
if not self.type_visibility.get(type_name, True):
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:
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:
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:
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:
gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)

View 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
View 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
View 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)