优化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

@@ -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()