mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-27 21:02:23 +00:00
Feature boundary test lxy (#128)
* Add Boundary validator * Boundary test ui * refect & optimize boundary validation
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user