import os import json import re from PyQt6.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLabel, QFrame, QGraphicsView, QGraphicsScene, QGraphicsPathItem) from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath def get_gcode_dir(): config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json") try: with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) return config.get("GCODE_DIR", "/home/lhye200/.octoprint/uploads") except: return "/home/lhye200/.octoprint/uploads" GCODE_DIR = get_gcode_dir() class GCode2DPreviewWidget(QGraphicsView): def __init__(self, parent=None): super().__init__(parent) self.scene = QGraphicsScene(self) self.setScene(self.scene) self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;") self.setRenderHint(QPainter.RenderHint.Antialiasing) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # 翻转Y轴让(0,0)位于左下角,适配物理坐标系 self.scale(1, -1) def draw_paths(self, lines_data): self.scene.clear() bounding_rect = QRectF() for color_str, data in lines_data.items(): points = data.get("points", []) line_width = data.get("line_width", 2) if not points: continue path = QPainterPath() # 判断是一组连续点的坐标(单线条)[[x1,y1], [x2,y2]] # 还是包含多根独立线条 [[[x1,y1], [x2,y2]], [[x3,y3], [x4,y4]]] if isinstance(points[0][0], (int, float)): path.moveTo(float(points[0][0]), float(points[0][1])) for pt in points[1:]: path.lineTo(float(pt[0]), float(pt[1])) else: for line_pts in points: if not line_pts: continue path.moveTo(float(line_pts[0][0]), float(line_pts[0][1])) for pt in line_pts[1:]: path.lineTo(float(pt[0]), float(pt[1])) path_item = QGraphicsPathItem(path) # 安全获取颜色 pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white") pen = QPen(pen_color) pen.setWidth(int(line_width)) # 保持 Cosmetic,以确保视图缩放时线条粗细在屏幕上看起来一致 pen.setCosmetic(True) path_item.setPen(pen) self.scene.addItem(path_item) bounding_rect = bounding_rect.united(path.boundingRect()) # 设置场景范围以便居中缩放 if bounding_rect.width() < 1 or bounding_rect.height() < 1: bounding_rect = QRectF(0, 0, 220, 220) bounding_rect.adjust(-10, -10, 10, 10) self.scene.setSceneRect(bounding_rect) self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) class StatusPage(QWidget): def __init__(self, api_client, parent=None): super().__init__(parent) self.api_client = api_client self.file_name = "None" self.progress = 0 self.display_name = "None" self.state = "Unknown" self.init_ui() self.timer = QTimer(self) self.timer.timeout.connect(self.update_status) self.timer.start(1000) self.update_status() def fresh_status_valve(self): data = self.api_client.get_status() # print("Status Data:", data) if data: status = data.get("status", {}) job = data.get("job", {}) self.file_name = job.get("job", {}).get("file", {}).get("name", "None") self.progress = job.get("progress", {}).get("completion", 0) self.display_name = job.get("job", {}).get("file", {}).get("display_name", "None") self.state = status.get("state", {}).get("text", "Offline") self.print_time = job.get("progress", {}).get("printTime", 0) self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0) # print(f"Updated Status: state={self.state}, file_name={self.file_name}, progress={self.progress:.1f}%, display_name={self.display_name}") def format_time(self, seconds): if seconds is None: return "N/A" m, s = divmod(seconds, 60) h, m = divmod(m, 60) if h > 0: return f"{int(h)}h {int(m)}m {int(s)}s" elif m > 0: return f"{int(m)}m {int(s)}s" else: return f"{int(s)}s" def init_ui(self): self.fresh_status_valve() main_layout = QHBoxLayout(self) self.left_frame = QFrame() self.left_frame.setStyleSheet("background-color: #444444; border-radius: 10px; color: white;") left_layout = QVBoxLayout(self.left_frame) self.lbl_status = QLabel(f"Status: {self.state}") left_layout.addWidget(self.lbl_status) self.lbl_job = QLabel(f"File: {self.display_name}") left_layout.addWidget(self.lbl_job) self.lbl_progress = QLabel(f"Progress: {self.progress if self.progress else 0:.1f}%") left_layout.addWidget(self.lbl_progress) self.lbl_print_time = QLabel(f"Print Time: {self.format_time(self.print_time)}") left_layout.addWidget(self.lbl_print_time) self.lbl_print_time_left = QLabel(f"Print Time Left: {self.format_time(self.print_time_left)}") left_layout.addWidget(self.lbl_print_time_left) left_layout.addStretch() #TODO: 3D Gcode View in right frame, use QML and QtQuick3D to render the Gcode vertices, pass the progress to QML to show the current layer # Due to the complexity of parsing Gcode and rendering it in 3D, this part will be implemented in a separate thread to avoid blocking the UI, and the vertices will be passed to QML for rendering. The progress will also be passed to QML to show the current layer being printed. # Load QtQuick3D View # self.right_frame = QFrame() # right_layout = QVBoxLayout(self.right_frame) # self.view = QQuickView() # self.view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) # qml_file = os.path.join(os.path.dirname(__file__), "gcode_view2.qml") # self.view.setSource(QUrl.fromLocalFile(qml_file)) # gcode_data = self.load_gcode_vertices(os.path.join(GCODE_DIR, self.file_name)) # self.view.rootContext().setContextProperty("gcodeData", gcode_data) # container = QWidget.createWindowContainer(self.view, self) # right_layout.addWidget(container) self.right_frame = QFrame() self.right_frame.setStyleSheet("background-color: #444444; border-radius: 10px;") right_layout = QVBoxLayout(self.right_frame) self.gcode_view = GCode2DPreviewWidget() right_layout.addWidget(self.gcode_view) main_layout.addWidget(self.left_frame, 1) main_layout.addWidget(self.right_frame, 2) def update_status(self): self.fresh_status_valve() self.lbl_status.setText(f"Status: {self.state}") self.lbl_job.setText(f"File: {self.display_name}") self.lbl_progress.setText(f"Progress: {self.progress if self.progress else 0:.1f}%") # Pass progress to QML # root_obj = self.view.rootObject() # if root_obj: # root_obj.setProperty("progress", prog if prog else 0) #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