mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-25 03:42:05 +00:00
Feature boundary test lxy (#128)
* Add Boundary validator * Boundary test ui * refect & optimize boundary validation
This commit is contained in:
312
tools/README_gcode_checker.md
Normal file
312
tools/README_gcode_checker.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# G-code边界超限检查工具 - 使用说明
|
||||
|
||||
## 📋 工具简介
|
||||
|
||||
这是一个带有图形界面的G-code边界检查工具,可以帮助你:
|
||||
|
||||
✅ **检测Travel移动超限** - 发现可能导致打印头撞机的Travel移动
|
||||
✅ **检测Extrude移动超限** - 发现挤出路径超出边界
|
||||
✅ **支持多种床类型** - 矩形床、圆形床(Delta打印机)
|
||||
✅ **详细报告** - 提供超限位置、类型、距离等详细信息
|
||||
✅ **快速预设** - 常见打印机尺寸一键设置
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方法1: 双击启动(推荐)
|
||||
|
||||
1. 双击 `run_gcode_checker.bat` 启动程序
|
||||
2. 如果提示"未找到Python",需要先安装Python(见下方)
|
||||
|
||||
### 方法2: 命令行启动
|
||||
|
||||
```bash
|
||||
python gcode_boundary_checker_gui.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 系统要求
|
||||
|
||||
- **Python 3.7+**(必需)
|
||||
- **tkinter**(Python标准库,通常自带)
|
||||
- 支持 Windows / macOS / Linux
|
||||
|
||||
### 安装Python
|
||||
|
||||
如果系统没有Python,请访问:https://www.python.org/downloads/
|
||||
|
||||
**Windows用户注意**:安装时勾选 "Add Python to PATH"
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
python --version
|
||||
# 应显示: Python 3.x.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 使用教程
|
||||
|
||||
### 步骤1: 选择G-code文件
|
||||
|
||||
点击"浏览..."按钮,选择要检查的`.gcode`文件
|
||||
|
||||
### 步骤2: 配置床参数
|
||||
|
||||
#### 矩形床(常见3D打印机)
|
||||
|
||||
1. 选择"矩形床"
|
||||
2. 输入尺寸:
|
||||
- **X**: 床宽度(mm)
|
||||
- **Y**: 床深度(mm)
|
||||
- **Z**: 最大打印高度(mm)
|
||||
3. 原点通常保持 (0, 0)
|
||||
|
||||
**快速预设**(点击即可应用):
|
||||
- `200×200×250` - Ender 3, CR-10等
|
||||
- `220×220×250` - Prusa i3 MK3等
|
||||
- `250×250×300` - CR-10S等
|
||||
- `300×300×400` - CR-10 Max等
|
||||
|
||||
#### 圆形床(Delta打印机)
|
||||
|
||||
1. 选择"圆形床 (Delta)"
|
||||
2. 输入:
|
||||
- **半径**: 床半径(mm)
|
||||
- **Z高度**: 最大打印高度(mm)
|
||||
|
||||
### 步骤3: 开始分析
|
||||
|
||||
1. 点击"开始分析"按钮
|
||||
2. 等待进度条完成(大文件可能需要几秒钟)
|
||||
3. 查看结果报告
|
||||
|
||||
### 步骤4: 查看结果
|
||||
|
||||
#### ✅ 正常情况
|
||||
```
|
||||
✅ 所有移动都在边界内!
|
||||
```
|
||||
|
||||
#### ⚠️ 发现超限
|
||||
报告会显示:
|
||||
- 总超限数量
|
||||
- Travel/Extrude超限分类
|
||||
- 超限类型统计
|
||||
- 详细超限列表(前100个)
|
||||
|
||||
每个超限包含:
|
||||
- **行号**: G-code文件中的行数
|
||||
- **类型**: Travel或Extrude
|
||||
- **位置**: X, Y, Z坐标
|
||||
- **超限类型**: X/Y/Z超限方向
|
||||
- **超出距离**: 超出边界多少mm
|
||||
- **原始代码**: 超限的G-code命令
|
||||
|
||||
### 步骤5: 保存报告
|
||||
|
||||
点击"保存报告"按钮,将完整报告保存为`.txt`文件
|
||||
|
||||
---
|
||||
|
||||
## 📊 报告示例
|
||||
|
||||
```
|
||||
======================================================================
|
||||
G-code边界超限分析报告
|
||||
======================================================================
|
||||
|
||||
床类型: 矩形
|
||||
床边界: X[0.0, 200.0] Y[0.0, 200.0] Z[0, 250.0]
|
||||
|
||||
总行数: 45823
|
||||
总移动数: 12456
|
||||
- Travel移动: 3421
|
||||
- Extrude移动: 9035
|
||||
|
||||
发现超限: 5 处
|
||||
- Travel超限: 3
|
||||
- Extrude超限: 2
|
||||
|
||||
超限类型统计:
|
||||
X > 最大值: 3 次
|
||||
Y > 最大值: 2 次
|
||||
|
||||
======================================================================
|
||||
详细超限列表 (前100个):
|
||||
======================================================================
|
||||
|
||||
[1] 行 1234: Travel - X > 最大值
|
||||
位置: X=205.340 Y=100.000 Z=50.000 E=123.456
|
||||
超出: 5.340 mm
|
||||
代码: G0 X205.34 Y100 F7200
|
||||
|
||||
[2] 行 2345: Extrude - Y > 最大值
|
||||
位置: X=150.000 Y=203.120 Z=50.000 E=150.234
|
||||
超出: 3.120 mm
|
||||
代码: G1 X150 Y203.12 E150.234
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 常见问题
|
||||
|
||||
### Q1: 为什么会检测出Travel超限?
|
||||
|
||||
**原因**:
|
||||
- OrcaSlicer原有代码只检查Extrude移动,忽略了Travel移动
|
||||
- Travel移动如果超限,可能导致打印头撞击边界
|
||||
|
||||
**如何修复**:
|
||||
1. 调整模型位置,远离床边缘
|
||||
2. 减小Skirt/Brim距离
|
||||
3. 检查擦料塔位置
|
||||
4. 调整打印顺序
|
||||
|
||||
### Q2: 显示"✅ 所有移动都在边界内",但切片软件仍报错?
|
||||
|
||||
可能原因:
|
||||
- 切片软件使用了更严格的边界检查
|
||||
- 考虑了挤出线宽(本工具只检查路径中心线)
|
||||
- 其他非边界问题(如对象冲突)
|
||||
|
||||
### Q3: 超限距离很小(如0.1mm),需要担心吗?
|
||||
|
||||
**一般情况**:
|
||||
- <0.5mm:通常是浮点误差,可能安全
|
||||
- 0.5-2mm:建议修复,有撞机风险
|
||||
- >2mm:必须修复
|
||||
|
||||
### Q4: 如何处理大量超限?
|
||||
|
||||
**排查步骤**:
|
||||
1. 检查床尺寸设置是否正确
|
||||
2. 检查模型是否整体偏移
|
||||
3. 检查切片配置(Skirt/Brim/Wipe Tower)
|
||||
4. 使用切片软件自动排版
|
||||
|
||||
### Q5: 程序运行很慢
|
||||
|
||||
**优化建议**:
|
||||
- 大文件(>100MB)可能需要1-2分钟
|
||||
- 关闭其他程序释放内存
|
||||
- Python版本建议3.9+(性能更好)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 高级用法
|
||||
|
||||
### 命令行版本
|
||||
|
||||
如果需要批量处理或集成到脚本,可以使用命令行版本:
|
||||
|
||||
```bash
|
||||
python analyze_gcode_bounds.py output.gcode --bed-size 200 200 250
|
||||
```
|
||||
|
||||
详细选项:
|
||||
```bash
|
||||
# 矩形床
|
||||
python analyze_gcode_bounds.py file.gcode --bed-size 200 200 250
|
||||
|
||||
# 矩形床 + 自定义原点
|
||||
python analyze_gcode_bounds.py file.gcode --bed-size 200 200 250 --bed-origin 10 10
|
||||
|
||||
# 圆形床
|
||||
python analyze_gcode_bounds.py file.gcode --bed-type circle --radius 100 --max-z 250
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 错误: "未找到Python"
|
||||
|
||||
**解决方法**:
|
||||
1. 安装Python 3.7+
|
||||
2. 确保安装时勾选"Add to PATH"
|
||||
3. 重启命令提示符/终端
|
||||
|
||||
### 错误: "No module named 'tkinter'"
|
||||
|
||||
**Windows**:重新安装Python,勾选"tcl/tk and IDLE"
|
||||
**Linux**:`sudo apt-get install python3-tk`
|
||||
**macOS**:通常自带,如缺失重新安装Python
|
||||
|
||||
### 界面显示乱码
|
||||
|
||||
修改系统区域设置为中文,或使用命令行版本
|
||||
|
||||
### 程序崩溃或卡死
|
||||
|
||||
1. 检查G-code文件是否损坏
|
||||
2. 尝试用文本编辑器打开G-code
|
||||
3. 更新Python到最新版本
|
||||
|
||||
---
|
||||
|
||||
## 📝 技术细节
|
||||
|
||||
### 检测算法
|
||||
|
||||
1. **矩形床**:检查每个移动点是否在 `[x_min, x_max] × [y_min, y_max] × [0, z_max]` 内
|
||||
2. **圆形床**:检查每个移动点到中心的距离是否 ≤ 半径
|
||||
3. **容差**:默认允许 0.01mm 误差(浮点精度)
|
||||
|
||||
### 移动分类
|
||||
|
||||
- **Travel**: G0命令 或 G1命令且E值不变
|
||||
- **Extrude**: G1/G2/G3命令且E值增加
|
||||
- **Retract**: E值减少(不检查)
|
||||
|
||||
### 性能
|
||||
|
||||
- 解析速度:约 50,000 行/秒(Python 3.9)
|
||||
- 内存占用:约为文件大小的 2-3倍
|
||||
- 大文件(1GB+)建议使用命令行版本
|
||||
|
||||
---
|
||||
|
||||
## 📄 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `gcode_boundary_checker_gui.py` | GUI版本(推荐) |
|
||||
| `analyze_gcode_bounds.py` | 命令行版本 |
|
||||
| `run_gcode_checker.bat` | Windows启动脚本 |
|
||||
| `README_gcode_checker.md` | 本说明文档 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- **OrcaSlicer修复文档**: `docs/gcode_boundary_optimization_implementation.md`
|
||||
- **技术方案**: `docs/gcode_boundary_checking_optimization.md`
|
||||
- **问题反馈**: https://github.com/Snapmaker/OrcaSlicer/issues
|
||||
|
||||
---
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
### v1.0 (2026-01-19)
|
||||
- ✅ 初始版本
|
||||
- ✅ 支持矩形床和圆形床
|
||||
- ✅ GUI界面
|
||||
- ✅ 详细报告生成
|
||||
- ✅ Travel/Extrude移动分类检测
|
||||
|
||||
---
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
本工具是OrcaSlicer边界检查优化项目的一部分,旨在帮助用户诊断和修复边界超限问题。
|
||||
|
||||
**项目编号**: ORCA-2026-001
|
||||
**创建日期**: 2026-01-19
|
||||
**作者**: Claude Code
|
||||
|
||||
---
|
||||
|
||||
**祝你打印顺利! 🎉**
|
||||
449
tools/analyze_gcode_bounds.py
Normal file
449
tools/analyze_gcode_bounds.py
Normal file
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
G-code边界超限分析工具
|
||||
Analyzes G-code files to find moves that exceed build volume boundaries.
|
||||
|
||||
用法 / Usage:
|
||||
python analyze_gcode_bounds.py <gcode_file> [options]
|
||||
|
||||
示例 / Examples:
|
||||
python analyze_gcode_bounds.py output.gcode --bed-size 200 200 250
|
||||
python analyze_gcode_bounds.py output.gcode --bed-type circle --radius 100
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple, Optional
|
||||
import math
|
||||
|
||||
|
||||
class BedType(Enum):
|
||||
RECTANGLE = "rectangle"
|
||||
CIRCLE = "circle"
|
||||
|
||||
class MoveType(Enum):
|
||||
TRAVEL = "Travel"
|
||||
EXTRUDE = "Extrude"
|
||||
ARC_CW = "Arc CW (G2)" # 顺时针弧线
|
||||
ARC_CCW = "Arc CCW (G3)" # 逆时针弧线
|
||||
RETRACT = "Retract"
|
||||
UNKNOWN = "Unknown"
|
||||
|
||||
class ViolationType(Enum):
|
||||
X_MIN = "X < Min"
|
||||
X_MAX = "X > Max"
|
||||
Y_MIN = "Y < Min"
|
||||
Y_MAX = "Y > Max"
|
||||
Z_MAX = "Z > Max"
|
||||
RADIUS = "Radius > Max (Circle bed)"
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
e: float = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Position(self.x, self.y, self.z, self.e)
|
||||
|
||||
@dataclass
|
||||
class Violation:
|
||||
line_num: int
|
||||
line_content: str
|
||||
position: Position
|
||||
move_type: MoveType
|
||||
violation_types: List[ViolationType]
|
||||
distance_out: float # 超出距离 (mm)
|
||||
|
||||
def __str__(self):
|
||||
vio_str = ", ".join([v.value for v in self.violation_types])
|
||||
return (f"Line {self.line_num}: {self.move_type.value} - {vio_str}\n"
|
||||
f" Position: X={self.position.x:.3f} Y={self.position.y:.3f} "
|
||||
f"Z={self.position.z:.3f} E={self.position.e:.3f}\n"
|
||||
f" Out by: {self.distance_out:.3f} mm\n"
|
||||
f" G-code: {self.line_content.strip()}")
|
||||
|
||||
|
||||
class GCodeAnalyzer:
|
||||
def __init__(self, bed_type: BedType, bed_min: Tuple[float, float],
|
||||
bed_max: Tuple[float, float], max_z: float, radius: float = None):
|
||||
self.bed_type = bed_type
|
||||
self.bed_min = bed_min
|
||||
self.bed_max = bed_max
|
||||
self.max_z = max_z
|
||||
self.radius = radius # For circle bed
|
||||
self.center = ((bed_max[0] + bed_min[0]) / 2,
|
||||
(bed_max[1] + bed_min[1]) / 2) if bed_type == BedType.CIRCLE else None
|
||||
|
||||
self.current_pos = Position()
|
||||
self.violations: List[Violation] = []
|
||||
|
||||
# 统计
|
||||
self.total_moves = 0
|
||||
self.travel_moves = 0
|
||||
self.extrude_moves = 0
|
||||
|
||||
def parse_gcode_file(self, filename: str):
|
||||
"""解析G-code文件"""
|
||||
print(f"正在分析文件: {filename}")
|
||||
print(f"床类型: {self.bed_type.value}")
|
||||
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
print(f"床边界: X[{self.bed_min[0]:.1f}, {self.bed_max[0]:.1f}] "
|
||||
f"Y[{self.bed_min[1]:.1f}, {self.bed_max[1]:.1f}] "
|
||||
f"Z[0, {self.max_z:.1f}]")
|
||||
else:
|
||||
print(f"床中心: ({self.center[0]:.1f}, {self.center[1]:.1f})")
|
||||
print(f"床半径: {self.radius:.1f} mm, Z[0, {self.max_z:.1f}]")
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
self._parse_line(line_num, line)
|
||||
except FileNotFoundError:
|
||||
print(f"错误: 文件未找到 '{filename}'")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"错误: 读取文件时出错: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _parse_line(self, line_num: int, line: str):
|
||||
"""解析单行G-code"""
|
||||
# 移除注释
|
||||
if ';' in line:
|
||||
code_part = line[:line.index(';')]
|
||||
comment = line[line.index(';'):]
|
||||
else:
|
||||
code_part = line
|
||||
comment = ""
|
||||
|
||||
code_part = code_part.strip().upper()
|
||||
if not code_part:
|
||||
return
|
||||
|
||||
# Check for G0/G1/G2/G3 commands (using word boundary to avoid matching G28, G29, etc.)
|
||||
g_match = re.match(r'G([0-3])\b', code_part)
|
||||
if not g_match:
|
||||
return
|
||||
|
||||
g_code = int(g_match.group(1))
|
||||
|
||||
# Parse coordinates
|
||||
x_match = re.search(r'X([-+]?\d*\.?\d+)', code_part)
|
||||
y_match = re.search(r'Y([-+]?\d*\.?\d+)', code_part)
|
||||
z_match = re.search(r'Z([-+]?\d*\.?\d+)', code_part)
|
||||
e_match = re.search(r'E([-+]?\d*\.?\d+)', code_part)
|
||||
i_match = re.search(r'I([-+]?\d*\.?\d+)', code_part)
|
||||
j_match = re.search(r'J([-+]?\d*\.?\d+)', code_part)
|
||||
|
||||
# G2/G3 arc commands
|
||||
if g_code in [2, 3] and (i_match or j_match):
|
||||
self._parse_arc(line_num, line, code_part, g_code, x_match, y_match,
|
||||
z_match, e_match, i_match, j_match)
|
||||
return
|
||||
|
||||
# G0/G1 linear moves
|
||||
new_pos = self.current_pos.copy()
|
||||
has_move = False
|
||||
has_xy_move = False
|
||||
|
||||
if x_match:
|
||||
new_pos.x = float(x_match.group(1))
|
||||
has_move = True
|
||||
has_xy_move = True
|
||||
if y_match:
|
||||
new_pos.y = float(y_match.group(1))
|
||||
has_move = True
|
||||
has_xy_move = True
|
||||
if z_match:
|
||||
new_pos.z = float(z_match.group(1))
|
||||
has_move = True
|
||||
if e_match:
|
||||
new_pos.e = float(e_match.group(1))
|
||||
|
||||
if not has_move:
|
||||
return
|
||||
|
||||
# 判断移动类型
|
||||
move_type = self._classify_move(code_part, self.current_pos, new_pos)
|
||||
|
||||
if move_type == MoveType.TRAVEL:
|
||||
self.travel_moves += 1
|
||||
elif move_type == MoveType.EXTRUDE:
|
||||
self.extrude_moves += 1
|
||||
self.total_moves += 1
|
||||
|
||||
# 检查边界 - Only check XY bounds if X or Y actually moved
|
||||
if has_xy_move:
|
||||
violations = self._check_bounds(new_pos)
|
||||
if violations:
|
||||
distance = self._calculate_distance_out(new_pos)
|
||||
self.violations.append(Violation(
|
||||
line_num=line_num,
|
||||
line_content=line,
|
||||
position=new_pos.copy(),
|
||||
move_type=move_type,
|
||||
violation_types=violations,
|
||||
distance_out=distance
|
||||
))
|
||||
|
||||
self.current_pos = new_pos
|
||||
|
||||
def _parse_arc(self, line_num: int, line: str, code_part: str, g_code: int,
|
||||
x_match, y_match, z_match, e_match, i_match, j_match):
|
||||
"""Parse G2/G3 arc commands and check arc path for boundary violations"""
|
||||
start_x = self.current_pos.x
|
||||
start_y = self.current_pos.y
|
||||
start_z = self.current_pos.z
|
||||
|
||||
i = float(i_match.group(1)) if i_match else 0.0
|
||||
j = float(j_match.group(1)) if j_match else 0.0
|
||||
|
||||
center_x = start_x + i
|
||||
center_y = start_y + j
|
||||
radius = math.sqrt(i * i + j * j)
|
||||
|
||||
end_x = float(x_match.group(1)) if x_match else None
|
||||
end_y = float(y_match.group(1)) if y_match else None
|
||||
end_z = float(z_match.group(1)) if z_match else start_z
|
||||
e = float(e_match.group(1)) if e_match else self.current_pos.e
|
||||
|
||||
if end_x is None and end_y is None:
|
||||
end_angle = math.atan2(start_y - center_y, start_x - center_x) + (2 * math.pi if g_code == 3 else -2 * math.pi)
|
||||
end_x = center_x + radius * math.cos(end_angle)
|
||||
end_y = center_y + radius * math.sin(end_angle)
|
||||
elif end_x is None:
|
||||
end_x = start_x
|
||||
elif end_y is None:
|
||||
end_y = start_y
|
||||
|
||||
start_angle = math.atan2(start_y - center_y, start_x - center_x)
|
||||
end_angle = math.atan2(end_y - center_y, end_x - center_x)
|
||||
|
||||
if g_code == 2:
|
||||
if end_angle > start_angle:
|
||||
end_angle -= 2 * math.pi
|
||||
angle_sweep = start_angle - end_angle
|
||||
else:
|
||||
if end_angle < start_angle:
|
||||
end_angle += 2 * math.pi
|
||||
angle_sweep = end_angle - start_angle
|
||||
|
||||
num_samples = max(8, int(abs(angle_sweep) * radius / 5))
|
||||
|
||||
move_type = MoveType.ARC_CCW if g_code == 3 else MoveType.ARC_CW
|
||||
if move_type == MoveType.ARC_CW:
|
||||
self.travel_moves += 1
|
||||
else:
|
||||
self.extrude_moves += 1
|
||||
self.total_moves += 1
|
||||
|
||||
for n in range(num_samples + 1):
|
||||
t = n / num_samples
|
||||
angle = start_angle + (angle_sweep * t if g_code == 3 else -angle_sweep * t)
|
||||
|
||||
sample_x = center_x + radius * math.cos(angle)
|
||||
sample_y = center_y + radius * math.sin(angle)
|
||||
sample_z = start_z + (end_z - start_z) * t
|
||||
|
||||
sample_pos = Position(sample_x, sample_y, sample_z, e)
|
||||
violations = self._check_bounds(sample_pos)
|
||||
|
||||
if violations:
|
||||
distance = self._calculate_distance_out(sample_pos)
|
||||
self.violations.append(Violation(
|
||||
line_num=line_num,
|
||||
line_content=line,
|
||||
position=sample_pos,
|
||||
move_type=move_type,
|
||||
violation_types=violations,
|
||||
distance_out=distance
|
||||
))
|
||||
break
|
||||
|
||||
self.current_pos.x = end_x
|
||||
self.current_pos.y = end_y
|
||||
self.current_pos.z = end_z
|
||||
self.current_pos.e = e
|
||||
|
||||
def _classify_move(self, code: str, old_pos: Position, new_pos: Position) -> MoveType:
|
||||
"""分类移动类型"""
|
||||
# G0 通常是快速移动(Travel)
|
||||
if code.startswith('G0'):
|
||||
return MoveType.TRAVEL
|
||||
|
||||
# G1 可能是Travel或Extrude,看E值
|
||||
if code.startswith('G1'):
|
||||
if abs(new_pos.e - old_pos.e) > 0.001: # 有挤出
|
||||
return MoveType.EXTRUDE
|
||||
else:
|
||||
return MoveType.TRAVEL
|
||||
|
||||
# G2/G3 是弧线,通常是挤出
|
||||
if code.startswith('G2') or code.startswith('G3'):
|
||||
return MoveType.EXTRUDE
|
||||
|
||||
return MoveType.UNKNOWN
|
||||
|
||||
def _check_bounds(self, pos: Position) -> List[ViolationType]:
|
||||
"""检查坐标是否超出边界"""
|
||||
violations = []
|
||||
epsilon = 0.01 # 允许的误差
|
||||
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
if pos.x < self.bed_min[0] - epsilon:
|
||||
violations.append(ViolationType.X_MIN)
|
||||
if pos.x > self.bed_max[0] + epsilon:
|
||||
violations.append(ViolationType.X_MAX)
|
||||
if pos.y < self.bed_min[1] - epsilon:
|
||||
violations.append(ViolationType.Y_MIN)
|
||||
if pos.y > self.bed_max[1] + epsilon:
|
||||
violations.append(ViolationType.Y_MAX)
|
||||
|
||||
elif self.bed_type == BedType.CIRCLE:
|
||||
dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2)
|
||||
if dist > self.radius + epsilon:
|
||||
violations.append(ViolationType.RADIUS)
|
||||
|
||||
# Z轴检查
|
||||
if self.max_z > 0 and pos.z > self.max_z + epsilon:
|
||||
violations.append(ViolationType.Z_MAX)
|
||||
|
||||
return violations
|
||||
|
||||
def _calculate_distance_out(self, pos: Position) -> float:
|
||||
"""计算超出边界的距离"""
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
dx = max(0, self.bed_min[0] - pos.x, pos.x - self.bed_max[0])
|
||||
dy = max(0, self.bed_min[1] - pos.y, pos.y - self.bed_max[1])
|
||||
dz = max(0, pos.z - self.max_z) if self.max_z > 0 else 0
|
||||
return math.sqrt(dx**2 + dy**2 + dz**2)
|
||||
|
||||
elif self.bed_type == BedType.CIRCLE:
|
||||
dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2)
|
||||
return max(0, dist - self.radius)
|
||||
|
||||
return 0.0
|
||||
|
||||
def print_report(self):
|
||||
"""打印分析报告"""
|
||||
print("\n" + "=" * 70)
|
||||
print("分析报告 / Analysis Report")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"\n总移动数: {self.total_moves}")
|
||||
print(f" - Travel移动: {self.travel_moves}")
|
||||
print(f" - Extrude移动: {self.extrude_moves}")
|
||||
print(f" - 其他: {self.total_moves - self.travel_moves - self.extrude_moves}")
|
||||
|
||||
print(f"\n发现超限: {len(self.violations)} 处")
|
||||
|
||||
if not self.violations:
|
||||
print("\n✅ 所有移动都在边界内!")
|
||||
return
|
||||
|
||||
# 按类型分组
|
||||
travel_violations = [v for v in self.violations if v.move_type == MoveType.TRAVEL]
|
||||
extrude_violations = [v for v in self.violations if v.move_type == MoveType.EXTRUDE]
|
||||
|
||||
print(f" - Travel超限: {len(travel_violations)}")
|
||||
print(f" - Extrude超限: {len(extrude_violations)}")
|
||||
|
||||
# 按超限类型统计
|
||||
print("\n超限类型统计:")
|
||||
from collections import Counter
|
||||
all_vio_types = []
|
||||
for v in self.violations:
|
||||
all_vio_types.extend(v.violation_types)
|
||||
vio_counter = Counter(all_vio_types)
|
||||
for vio_type, count in vio_counter.most_common():
|
||||
print(f" {vio_type.value}: {count} 次")
|
||||
|
||||
# 详细列出超限
|
||||
print("\n" + "=" * 70)
|
||||
print("详细超限列表 (前50个):")
|
||||
print("=" * 70)
|
||||
|
||||
for i, violation in enumerate(self.violations[:50], 1):
|
||||
print(f"\n[{i}] {violation}")
|
||||
|
||||
if len(self.violations) > 50:
|
||||
print(f"\n... 还有 {len(self.violations) - 50} 个超限未显示")
|
||||
|
||||
# 保存到文件
|
||||
output_file = "gcode_violations.txt"
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("G-code边界超限详细报告\n")
|
||||
f.write("=" * 70 + "\n\n")
|
||||
|
||||
for i, violation in enumerate(self.violations, 1):
|
||||
f.write(f"[{i}] {violation}\n\n")
|
||||
|
||||
print(f"\n完整报告已保存到: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='分析G-code文件的边界超限问题',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
# 矩形床 200x200x250mm
|
||||
python %(prog)s output.gcode --bed-size 200 200 250
|
||||
|
||||
# 矩形床,指定原点偏移
|
||||
python %(prog)s output.gcode --bed-size 200 200 250 --bed-origin 0 0
|
||||
|
||||
# 圆形床(如Delta打印机)
|
||||
python %(prog)s output.gcode --bed-type circle --radius 100 --max-z 250
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('gcode_file', help='G-code文件路径')
|
||||
parser.add_argument('--bed-type', choices=['rectangle', 'circle'],
|
||||
default='rectangle', help='床类型 (默认: rectangle)')
|
||||
parser.add_argument('--bed-size', type=float, nargs=3, metavar=('X', 'Y', 'Z'),
|
||||
help='床尺寸 X Y Z (mm), 例如: 200 200 250')
|
||||
parser.add_argument('--bed-origin', type=float, nargs=2, metavar=('X', 'Y'),
|
||||
default=(0, 0), help='床原点坐标 (默认: 0 0)')
|
||||
parser.add_argument('--radius', type=float, help='圆形床半径 (mm)')
|
||||
parser.add_argument('--max-z', type=float, help='最大Z高度 (mm)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 解析床参数
|
||||
bed_type = BedType(args.bed_type)
|
||||
|
||||
if bed_type == BedType.RECTANGLE:
|
||||
if not args.bed_size:
|
||||
print("错误: 矩形床需要指定 --bed-size")
|
||||
sys.exit(1)
|
||||
bed_min = (args.bed_origin[0], args.bed_origin[1])
|
||||
bed_max = (args.bed_origin[0] + args.bed_size[0],
|
||||
args.bed_origin[1] + args.bed_size[1])
|
||||
max_z = args.bed_size[2]
|
||||
radius = None
|
||||
|
||||
elif bed_type == BedType.CIRCLE:
|
||||
if not args.radius or not args.max_z:
|
||||
print("错误: 圆形床需要指定 --radius 和 --max-z")
|
||||
sys.exit(1)
|
||||
bed_min = (-args.radius, -args.radius)
|
||||
bed_max = (args.radius, args.radius)
|
||||
max_z = args.max_z
|
||||
radius = args.radius
|
||||
|
||||
# 分析G-code
|
||||
analyzer = GCodeAnalyzer(bed_type, bed_min, bed_max, max_z, radius)
|
||||
analyzer.parse_gcode_file(args.gcode_file)
|
||||
analyzer.print_report()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
674
tools/gcode_boundary_checker_gui.py
Normal file
674
tools/gcode_boundary_checker_gui.py
Normal file
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
G-code边界超限检查工具 - GUI版本
|
||||
G-code Boundary Violation Checker - GUI Version
|
||||
|
||||
带有图形界面的G-code边界检测工具
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import re
|
||||
import math
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class BedType(Enum):
|
||||
RECTANGLE = "矩形床 (Rectangle)"
|
||||
CIRCLE = "圆形床 (Circle)"
|
||||
|
||||
|
||||
class MoveType(Enum):
|
||||
TRAVEL = "Travel"
|
||||
EXTRUDE = "Extrude"
|
||||
ARC_CW = "Arc CW (G2)" # 顺时针弧线
|
||||
ARC_CCW = "Arc CCW (G3)" # 逆时针弧线
|
||||
RETRACT = "Retract"
|
||||
UNKNOWN = "Unknown"
|
||||
|
||||
|
||||
class ViolationType(Enum):
|
||||
X_MIN = "X < 最小值"
|
||||
X_MAX = "X > 最大值"
|
||||
Y_MIN = "Y < 最小值"
|
||||
Y_MAX = "Y > 最大值"
|
||||
Z_MAX = "Z > 最大值"
|
||||
RADIUS = "半径超限 (圆形床)"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
e: float = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Position(self.x, self.y, self.z, self.e)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Violation:
|
||||
line_num: int
|
||||
line_content: str
|
||||
position: Position
|
||||
move_type: MoveType
|
||||
violation_types: List[ViolationType]
|
||||
distance_out: float
|
||||
|
||||
def __str__(self):
|
||||
vio_str = ", ".join([v.value for v in self.violation_types])
|
||||
return (f"行 {self.line_num}: {self.move_type.value} - {vio_str}\n"
|
||||
f" 位置: X={self.position.x:.3f} Y={self.position.y:.3f} "
|
||||
f"Z={self.position.z:.3f} E={self.position.e:.3f}\n"
|
||||
f" 超出: {self.distance_out:.3f} mm\n"
|
||||
f" 代码: {self.line_content.strip()}")
|
||||
|
||||
|
||||
class GCodeAnalyzer:
|
||||
def __init__(self, bed_type: BedType, bed_min: Tuple[float, float],
|
||||
bed_max: Tuple[float, float], max_z: float, radius: float = None,
|
||||
progress_callback=None):
|
||||
self.bed_type = bed_type
|
||||
self.bed_min = bed_min
|
||||
self.bed_max = bed_max
|
||||
self.max_z = max_z
|
||||
self.radius = radius
|
||||
self.center = ((bed_max[0] + bed_min[0]) / 2,
|
||||
(bed_max[1] + bed_min[1]) / 2) if bed_type == BedType.CIRCLE else None
|
||||
self.progress_callback = progress_callback
|
||||
|
||||
self.current_pos = Position()
|
||||
self.violations: List[Violation] = []
|
||||
self.total_moves = 0
|
||||
self.travel_moves = 0
|
||||
self.extrude_moves = 0
|
||||
self.total_lines = 0
|
||||
|
||||
def parse_gcode_file(self, filename: str):
|
||||
"""解析G-code文件"""
|
||||
try:
|
||||
# 先计算总行数
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
self.total_lines = sum(1 for _ in f)
|
||||
|
||||
# 解析文件
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
self._parse_line(line_num, line)
|
||||
|
||||
# 更新进度
|
||||
if self.progress_callback and line_num % 100 == 0:
|
||||
progress = (line_num / self.total_lines) * 100
|
||||
self.progress_callback(progress, line_num, self.total_lines)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
def _parse_line(self, line_num: int, line: str):
|
||||
"""解析单行G-code"""
|
||||
if ';' in line:
|
||||
code_part = line[:line.index(';')]
|
||||
else:
|
||||
code_part = line
|
||||
|
||||
code_part = code_part.strip().upper()
|
||||
if not code_part:
|
||||
return
|
||||
|
||||
# Check for G0/G1/G2/G3 commands (using word boundary to avoid matching G28, G29, etc.)
|
||||
g_match = re.match(r'G([0-3])\b', code_part)
|
||||
if not g_match:
|
||||
return
|
||||
|
||||
g_code = int(g_match.group(1))
|
||||
|
||||
# Parse coordinates
|
||||
x_match = re.search(r'X([-+]?\d*\.?\d+)', code_part)
|
||||
y_match = re.search(r'Y([-+]?\d*\.?\d+)', code_part)
|
||||
z_match = re.search(r'Z([-+]?\d*\.?\d+)', code_part)
|
||||
e_match = re.search(r'E([-+]?\d*\.?\d+)', code_part)
|
||||
i_match = re.search(r'I([-+]?\d*\.?\d+)', code_part)
|
||||
j_match = re.search(r'J([-+]?\d*\.?\d+)', code_part)
|
||||
|
||||
# G2/G3 arc commands
|
||||
if g_code in [2, 3] and (i_match or j_match):
|
||||
self._parse_arc(line_num, line, code_part, g_code, x_match, y_match,
|
||||
z_match, e_match, i_match, j_match)
|
||||
return
|
||||
|
||||
# G0/G1 linear moves
|
||||
new_pos = self.current_pos.copy()
|
||||
has_move = False
|
||||
has_xy_move = False
|
||||
|
||||
if x_match:
|
||||
new_pos.x = float(x_match.group(1))
|
||||
has_move = True
|
||||
has_xy_move = True
|
||||
if y_match:
|
||||
new_pos.y = float(y_match.group(1))
|
||||
has_move = True
|
||||
has_xy_move = True
|
||||
if z_match:
|
||||
new_pos.z = float(z_match.group(1))
|
||||
has_move = True
|
||||
if e_match:
|
||||
new_pos.e = float(e_match.group(1))
|
||||
|
||||
if not has_move:
|
||||
return
|
||||
|
||||
move_type = self._classify_move(code_part, self.current_pos, new_pos)
|
||||
|
||||
if move_type == MoveType.TRAVEL:
|
||||
self.travel_moves += 1
|
||||
elif move_type == MoveType.EXTRUDE:
|
||||
self.extrude_moves += 1
|
||||
self.total_moves += 1
|
||||
|
||||
# Only check XY bounds if X or Y actually moved
|
||||
if has_xy_move:
|
||||
violations = self._check_bounds(new_pos)
|
||||
if violations:
|
||||
distance = self._calculate_distance_out(new_pos)
|
||||
self.violations.append(Violation(
|
||||
line_num=line_num,
|
||||
line_content=line,
|
||||
position=new_pos.copy(),
|
||||
move_type=move_type,
|
||||
violation_types=violations,
|
||||
distance_out=distance
|
||||
))
|
||||
|
||||
self.current_pos = new_pos
|
||||
|
||||
def _parse_arc(self, line_num: int, line: str, code_part: str, g_code: int,
|
||||
x_match, y_match, z_match, e_match, i_match, j_match):
|
||||
"""Parse G2/G3 arc commands and check arc path for boundary violations"""
|
||||
# Current position is the start of the arc
|
||||
start_x = self.current_pos.x
|
||||
start_y = self.current_pos.y
|
||||
start_z = self.current_pos.z
|
||||
|
||||
# Parse I, J (offsets from start to center)
|
||||
i = float(i_match.group(1)) if i_match else 0.0
|
||||
j = float(j_match.group(1)) if j_match else 0.0
|
||||
|
||||
# Calculate arc center
|
||||
center_x = start_x + i
|
||||
center_y = start_y + j
|
||||
radius = math.sqrt(i * i + j * j)
|
||||
|
||||
# Parse X, Y if present (end point)
|
||||
end_x = float(x_match.group(1)) if x_match else None
|
||||
end_y = float(y_match.group(1)) if y_match else None
|
||||
end_z = float(z_match.group(1)) if z_match else start_z
|
||||
e = float(e_match.group(1)) if e_match else self.current_pos.e
|
||||
|
||||
# If no X/Y specified, do a full circle (360 degrees)
|
||||
if end_x is None and end_y is None:
|
||||
# For full circle, calculate end point as start point
|
||||
end_angle = math.atan2(start_y - center_y, start_x - center_x) + (2 * math.pi if g_code == 3 else -2 * math.pi)
|
||||
end_x = center_x + radius * math.cos(end_angle)
|
||||
end_y = center_y + radius * math.sin(end_angle)
|
||||
elif end_x is None:
|
||||
end_x = start_x
|
||||
elif end_y is None:
|
||||
end_y = start_y
|
||||
|
||||
# Calculate start and end angles
|
||||
start_angle = math.atan2(start_y - center_y, start_x - center_x)
|
||||
end_angle = math.atan2(end_y - center_y, end_x - center_x)
|
||||
|
||||
# Determine arc direction and angle sweep
|
||||
if g_code == 2: # Clockwise
|
||||
if end_angle > start_angle:
|
||||
end_angle -= 2 * math.pi
|
||||
angle_sweep = start_angle - end_angle
|
||||
else: # G3: Counter-clockwise
|
||||
if end_angle < start_angle:
|
||||
end_angle += 2 * math.pi
|
||||
angle_sweep = end_angle - start_angle
|
||||
|
||||
# Sample points along the arc and check each
|
||||
num_samples = max(8, int(abs(angle_sweep) * radius / 5)) # At least 8 points, or 1 per 5mm of arc length
|
||||
|
||||
move_type = MoveType.ARC_CCW if g_code == 3 else MoveType.ARC_CW
|
||||
if move_type == MoveType.ARC_CW:
|
||||
self.travel_moves += 1
|
||||
else:
|
||||
self.extrude_moves += 1
|
||||
self.total_moves += 1
|
||||
|
||||
# Check arc samples
|
||||
for n in range(num_samples + 1):
|
||||
t = n / num_samples
|
||||
angle = start_angle + (angle_sweep * t if g_code == 3 else -angle_sweep * t)
|
||||
|
||||
sample_x = center_x + radius * math.cos(angle)
|
||||
sample_y = center_y + radius * math.sin(angle)
|
||||
sample_z = start_z + (end_z - start_z) * t # Interpolate Z
|
||||
|
||||
# Check this point
|
||||
sample_pos = Position(sample_x, sample_y, sample_z, e)
|
||||
violations = self._check_bounds(sample_pos)
|
||||
|
||||
if violations:
|
||||
distance = self._calculate_distance_out(sample_pos)
|
||||
self.violations.append(Violation(
|
||||
line_num=line_num,
|
||||
line_content=line,
|
||||
position=sample_pos,
|
||||
move_type=move_type,
|
||||
violation_types=violations,
|
||||
distance_out=distance
|
||||
))
|
||||
break # Only record first violation on this arc
|
||||
|
||||
# Update current position to arc end
|
||||
self.current_pos.x = end_x
|
||||
self.current_pos.y = end_y
|
||||
self.current_pos.z = end_z
|
||||
self.current_pos.e = e
|
||||
|
||||
def _classify_move(self, code: str, old_pos: Position, new_pos: Position) -> MoveType:
|
||||
if code.startswith('G0'):
|
||||
return MoveType.TRAVEL
|
||||
if code.startswith('G1'):
|
||||
if abs(new_pos.e - old_pos.e) > 0.001:
|
||||
return MoveType.EXTRUDE
|
||||
else:
|
||||
return MoveType.TRAVEL
|
||||
if code.startswith('G2') or code.startswith('G3'):
|
||||
return MoveType.EXTRUDE
|
||||
return MoveType.UNKNOWN
|
||||
|
||||
def _check_bounds(self, pos: Position) -> List[ViolationType]:
|
||||
violations = []
|
||||
epsilon = 0.01
|
||||
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
if pos.x < self.bed_min[0] - epsilon:
|
||||
violations.append(ViolationType.X_MIN)
|
||||
if pos.x > self.bed_max[0] + epsilon:
|
||||
violations.append(ViolationType.X_MAX)
|
||||
if pos.y < self.bed_min[1] - epsilon:
|
||||
violations.append(ViolationType.Y_MIN)
|
||||
if pos.y > self.bed_max[1] + epsilon:
|
||||
violations.append(ViolationType.Y_MAX)
|
||||
elif self.bed_type == BedType.CIRCLE:
|
||||
dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2)
|
||||
if dist > self.radius + epsilon:
|
||||
violations.append(ViolationType.RADIUS)
|
||||
|
||||
if self.max_z > 0 and pos.z > self.max_z + epsilon:
|
||||
violations.append(ViolationType.Z_MAX)
|
||||
|
||||
return violations
|
||||
|
||||
def _calculate_distance_out(self, pos: Position) -> float:
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
dx = max(0, self.bed_min[0] - pos.x, pos.x - self.bed_max[0])
|
||||
dy = max(0, self.bed_min[1] - pos.y, pos.y - self.bed_max[1])
|
||||
dz = max(0, pos.z - self.max_z) if self.max_z > 0 else 0
|
||||
return math.sqrt(dx**2 + dy**2 + dz**2)
|
||||
elif self.bed_type == BedType.CIRCLE:
|
||||
dist = math.sqrt((pos.x - self.center[0])**2 + (pos.y - self.center[1])**2)
|
||||
return max(0, dist - self.radius)
|
||||
return 0.0
|
||||
|
||||
def get_report(self) -> str:
|
||||
"""生成报告"""
|
||||
report = []
|
||||
report.append("=" * 70)
|
||||
report.append("G-code边界超限分析报告")
|
||||
report.append("=" * 70)
|
||||
report.append("")
|
||||
|
||||
# 床信息
|
||||
if self.bed_type == BedType.RECTANGLE:
|
||||
report.append(f"床类型: 矩形")
|
||||
report.append(f"床边界: X[{self.bed_min[0]:.1f}, {self.bed_max[0]:.1f}] "
|
||||
f"Y[{self.bed_min[1]:.1f}, {self.bed_max[1]:.1f}] "
|
||||
f"Z[0, {self.max_z:.1f}]")
|
||||
else:
|
||||
report.append(f"床类型: 圆形")
|
||||
report.append(f"床中心: ({self.center[0]:.1f}, {self.center[1]:.1f})")
|
||||
report.append(f"床半径: {self.radius:.1f} mm, Z[0, {self.max_z:.1f}]")
|
||||
|
||||
report.append("")
|
||||
report.append(f"总行数: {self.total_lines}")
|
||||
report.append(f"总移动数: {self.total_moves}")
|
||||
report.append(f" - Travel移动: {self.travel_moves}")
|
||||
report.append(f" - Extrude移动: {self.extrude_moves}")
|
||||
report.append("")
|
||||
|
||||
# 超限统计
|
||||
report.append(f"发现超限: {len(self.violations)} 处")
|
||||
|
||||
if not self.violations:
|
||||
report.append("")
|
||||
report.append("✅ 所有移动都在边界内!")
|
||||
return "\n".join(report)
|
||||
|
||||
travel_violations = [v for v in self.violations if v.move_type == MoveType.TRAVEL]
|
||||
extrude_violations = [v for v in self.violations if v.move_type == MoveType.EXTRUDE]
|
||||
|
||||
report.append(f" - Travel超限: {len(travel_violations)}")
|
||||
report.append(f" - Extrude超限: {len(extrude_violations)}")
|
||||
report.append("")
|
||||
|
||||
# 超限类型统计
|
||||
from collections import Counter
|
||||
all_vio_types = []
|
||||
for v in self.violations:
|
||||
all_vio_types.extend(v.violation_types)
|
||||
vio_counter = Counter(all_vio_types)
|
||||
|
||||
report.append("超限类型统计:")
|
||||
for vio_type, count in vio_counter.most_common():
|
||||
report.append(f" {vio_type.value}: {count} 次")
|
||||
report.append("")
|
||||
|
||||
# 详细列表(前100个)
|
||||
report.append("=" * 70)
|
||||
report.append(f"详细超限列表 (前100个):")
|
||||
report.append("=" * 70)
|
||||
report.append("")
|
||||
|
||||
for i, violation in enumerate(self.violations[:100], 1):
|
||||
report.append(f"[{i}] {violation}")
|
||||
report.append("")
|
||||
|
||||
if len(self.violations) > 100:
|
||||
report.append(f"... 还有 {len(self.violations) - 100} 个超限未显示")
|
||||
|
||||
return "\n".join(report)
|
||||
|
||||
|
||||
class GCodeBoundaryCheckerGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("G-code边界超限检查工具")
|
||||
self.root.geometry("900x700")
|
||||
|
||||
# 变量
|
||||
self.gcode_file = tk.StringVar()
|
||||
self.bed_type = tk.StringVar(value="rectangle")
|
||||
self.bed_x = tk.DoubleVar(value=200.0)
|
||||
self.bed_y = tk.DoubleVar(value=200.0)
|
||||
self.bed_z = tk.DoubleVar(value=250.0)
|
||||
self.bed_radius = tk.DoubleVar(value=100.0)
|
||||
self.origin_x = tk.DoubleVar(value=0.0)
|
||||
self.origin_y = tk.DoubleVar(value=0.0)
|
||||
|
||||
self.analyzer = None
|
||||
self.analyzing = False
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 主框架
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(4, weight=1)
|
||||
|
||||
# 1. 文件选择
|
||||
file_frame = ttk.LabelFrame(main_frame, text="1. 选择G-code文件", padding="10")
|
||||
file_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
file_frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Entry(file_frame, textvariable=self.gcode_file, width=50).grid(
|
||||
row=0, column=0, sticky=(tk.W, tk.E), padx=5)
|
||||
ttk.Button(file_frame, text="浏览...", command=self.browse_file).grid(
|
||||
row=0, column=1, padx=5)
|
||||
|
||||
# 2. 床参数
|
||||
bed_frame = ttk.LabelFrame(main_frame, text="2. 床参数配置", padding="10")
|
||||
bed_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
# 床类型选择
|
||||
type_frame = ttk.Frame(bed_frame)
|
||||
type_frame.grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Label(type_frame, text="床类型:").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(type_frame, text="矩形床", variable=self.bed_type,
|
||||
value="rectangle", command=self.update_bed_type).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(type_frame, text="圆形床 (Delta)", variable=self.bed_type,
|
||||
value="circle", command=self.update_bed_type).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 矩形床参数
|
||||
self.rect_frame = ttk.Frame(bed_frame)
|
||||
self.rect_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Label(self.rect_frame, text="尺寸 (mm):").grid(row=0, column=0, padx=5)
|
||||
ttk.Label(self.rect_frame, text="X:").grid(row=0, column=1)
|
||||
ttk.Entry(self.rect_frame, textvariable=self.bed_x, width=10).grid(row=0, column=2, padx=2)
|
||||
ttk.Label(self.rect_frame, text="Y:").grid(row=0, column=3)
|
||||
ttk.Entry(self.rect_frame, textvariable=self.bed_y, width=10).grid(row=0, column=4, padx=2)
|
||||
ttk.Label(self.rect_frame, text="Z:").grid(row=0, column=5)
|
||||
ttk.Entry(self.rect_frame, textvariable=self.bed_z, width=10).grid(row=0, column=6, padx=2)
|
||||
|
||||
ttk.Label(self.rect_frame, text="原点:").grid(row=1, column=0, padx=5, pady=5)
|
||||
ttk.Label(self.rect_frame, text="X:").grid(row=1, column=1)
|
||||
ttk.Entry(self.rect_frame, textvariable=self.origin_x, width=10).grid(row=1, column=2, padx=2)
|
||||
ttk.Label(self.rect_frame, text="Y:").grid(row=1, column=3)
|
||||
ttk.Entry(self.rect_frame, textvariable=self.origin_y, width=10).grid(row=1, column=4, padx=2)
|
||||
|
||||
# 圆形床参数
|
||||
self.circle_frame = ttk.Frame(bed_frame)
|
||||
self.circle_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Label(self.circle_frame, text="半径 (mm):").grid(row=0, column=0, padx=5)
|
||||
ttk.Entry(self.circle_frame, textvariable=self.bed_radius, width=10).grid(row=0, column=1, padx=2)
|
||||
ttk.Label(self.circle_frame, text="Z高度:").grid(row=0, column=2, padx=5)
|
||||
ttk.Entry(self.circle_frame, textvariable=self.bed_z, width=10).grid(row=0, column=3, padx=2)
|
||||
|
||||
self.circle_frame.grid_remove() # 初始隐藏
|
||||
|
||||
# 快速预设
|
||||
preset_frame = ttk.Frame(bed_frame)
|
||||
preset_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Label(preset_frame, text="快速预设:").pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(preset_frame, text="200×200×250",
|
||||
command=lambda: self.apply_preset(200, 200, 250)).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(preset_frame, text="220×220×250",
|
||||
command=lambda: self.apply_preset(220, 220, 250)).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(preset_frame, text="250×250×300",
|
||||
command=lambda: self.apply_preset(250, 250, 300)).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(preset_frame, text="300×300×400",
|
||||
command=lambda: self.apply_preset(300, 300, 400)).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# 3. 控制按钮
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.grid(row=2, column=0, pady=10)
|
||||
|
||||
self.analyze_btn = ttk.Button(control_frame, text="开始分析",
|
||||
command=self.start_analysis, width=15)
|
||||
self.analyze_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.save_btn = ttk.Button(control_frame, text="保存报告",
|
||||
command=self.save_report, width=15, state=tk.DISABLED)
|
||||
self.save_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
ttk.Button(control_frame, text="清除结果",
|
||||
command=self.clear_results, width=15).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 4. 进度条
|
||||
self.progress_var = tk.DoubleVar()
|
||||
self.progress_label = ttk.Label(main_frame, text="")
|
||||
self.progress_label.grid(row=3, column=0, sticky=tk.W)
|
||||
|
||||
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var,
|
||||
maximum=100, mode='determinate')
|
||||
self.progress_bar.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
# 5. 结果显示
|
||||
result_frame = ttk.LabelFrame(main_frame, text="分析结果", padding="10")
|
||||
result_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
result_frame.columnconfigure(0, weight=1)
|
||||
result_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.result_text = scrolledtext.ScrolledText(result_frame, width=80, height=20,
|
||||
font=('Courier New', 9))
|
||||
self.result_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
def browse_file(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="选择G-code文件",
|
||||
filetypes=[("G-code文件", "*.gcode *.GCODE *.gco *.GCO"),
|
||||
("所有文件", "*.*")]
|
||||
)
|
||||
if filename:
|
||||
self.gcode_file.set(filename)
|
||||
|
||||
def update_bed_type(self):
|
||||
if self.bed_type.get() == "rectangle":
|
||||
self.rect_frame.grid()
|
||||
self.circle_frame.grid_remove()
|
||||
else:
|
||||
self.rect_frame.grid_remove()
|
||||
self.circle_frame.grid()
|
||||
|
||||
def apply_preset(self, x, y, z):
|
||||
self.bed_x.set(x)
|
||||
self.bed_y.set(y)
|
||||
self.bed_z.set(z)
|
||||
self.bed_type.set("rectangle")
|
||||
self.update_bed_type()
|
||||
|
||||
def update_progress(self, progress, current, total):
|
||||
self.progress_var.set(progress)
|
||||
self.progress_label.config(text=f"正在分析... {current}/{total} 行 ({progress:.1f}%)")
|
||||
|
||||
def start_analysis(self):
|
||||
# 验证输入
|
||||
if not self.gcode_file.get():
|
||||
messagebox.showerror("错误", "请选择G-code文件")
|
||||
return
|
||||
|
||||
if not Path(self.gcode_file.get()).exists():
|
||||
messagebox.showerror("错误", "文件不存在")
|
||||
return
|
||||
|
||||
# 禁用按钮
|
||||
self.analyze_btn.config(state=tk.DISABLED)
|
||||
self.save_btn.config(state=tk.DISABLED)
|
||||
self.result_text.delete(1.0, tk.END)
|
||||
self.progress_var.set(0)
|
||||
|
||||
# 在后台线程中分析
|
||||
thread = threading.Thread(target=self.run_analysis)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def run_analysis(self):
|
||||
try:
|
||||
# 准备参数
|
||||
if self.bed_type.get() == "rectangle":
|
||||
bed_type = BedType.RECTANGLE
|
||||
bed_min = (self.origin_x.get(), self.origin_y.get())
|
||||
bed_max = (self.origin_x.get() + self.bed_x.get(),
|
||||
self.origin_y.get() + self.bed_y.get())
|
||||
max_z = self.bed_z.get()
|
||||
radius = None
|
||||
else:
|
||||
bed_type = BedType.CIRCLE
|
||||
radius = self.bed_radius.get()
|
||||
bed_min = (-radius, -radius)
|
||||
bed_max = (radius, radius)
|
||||
max_z = self.bed_z.get()
|
||||
|
||||
# 创建分析器
|
||||
self.analyzer = GCodeAnalyzer(bed_type, bed_min, bed_max, max_z, radius,
|
||||
progress_callback=self.update_progress)
|
||||
|
||||
# 分析文件
|
||||
result = self.analyzer.parse_gcode_file(self.gcode_file.get())
|
||||
|
||||
if result is not True:
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", f"分析失败: {result}"))
|
||||
self.root.after(0, lambda: self.analyze_btn.config(state=tk.NORMAL))
|
||||
return
|
||||
|
||||
# 生成报告
|
||||
report = self.analyzer.get_report()
|
||||
|
||||
# 更新UI
|
||||
self.root.after(0, lambda: self.display_results(report))
|
||||
|
||||
except Exception as e:
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", f"分析出错: {str(e)}"))
|
||||
self.root.after(0, lambda: self.analyze_btn.config(state=tk.NORMAL))
|
||||
|
||||
def display_results(self, report):
|
||||
self.result_text.delete(1.0, tk.END)
|
||||
self.result_text.insert(1.0, report)
|
||||
|
||||
# 高亮显示
|
||||
if "✅" in report:
|
||||
self.result_text.tag_config("success", foreground="green", font=('Courier New', 9, 'bold'))
|
||||
start = self.result_text.search("✅", 1.0, tk.END)
|
||||
if start:
|
||||
end = f"{start}+1c"
|
||||
self.result_text.tag_add("success", start, end)
|
||||
|
||||
self.progress_label.config(text="分析完成!")
|
||||
self.progress_var.set(100)
|
||||
self.analyze_btn.config(state=tk.NORMAL)
|
||||
self.save_btn.config(state=tk.NORMAL)
|
||||
|
||||
# 如果有超限,弹出提示
|
||||
if self.analyzer and len(self.analyzer.violations) > 0:
|
||||
messagebox.showwarning("发现超限",
|
||||
f"发现 {len(self.analyzer.violations)} 处边界超限!\n"
|
||||
f"请查看详细报告。")
|
||||
else:
|
||||
messagebox.showinfo("检查完成", "✅ 所有移动都在边界内!")
|
||||
|
||||
def save_report(self):
|
||||
if not self.analyzer:
|
||||
return
|
||||
|
||||
filename = filedialog.asksaveasfilename(
|
||||
title="保存报告",
|
||||
defaultextension=".txt",
|
||||
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
||||
initialfile="gcode_boundary_report.txt"
|
||||
)
|
||||
|
||||
if filename:
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(self.result_text.get(1.0, tk.END))
|
||||
messagebox.showinfo("保存成功", f"报告已保存到:\n{filename}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("保存失败", f"保存出错: {str(e)}")
|
||||
|
||||
def clear_results(self):
|
||||
self.result_text.delete(1.0, tk.END)
|
||||
self.progress_var.set(0)
|
||||
self.progress_label.config(text="")
|
||||
self.analyzer = None
|
||||
self.save_btn.config(state=tk.DISABLED)
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = GCodeBoundaryCheckerGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
28
tools/run_gcode_checker.bat
Normal file
28
tools/run_gcode_checker.bat
Normal file
@@ -0,0 +1,28 @@
|
||||
@echo off
|
||||
REM G-code边界检查工具 - Windows启动脚本
|
||||
|
||||
echo ========================================
|
||||
echo G-code边界超限检查工具
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 检查Python是否安装
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 错误: 未找到Python
|
||||
echo 请先安装Python 3.7或更高版本
|
||||
echo 下载地址: https://www.python.org/downloads/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 启动GUI界面...
|
||||
echo.
|
||||
|
||||
python "%~dp0gcode_boundary_checker_gui.py"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo 程序运行出错,按任意键退出...
|
||||
pause
|
||||
)
|
||||
Reference in New Issue
Block a user