Revert "Feature boundary test lxy (#128)"

This reverts commit a1769a2148.
This commit is contained in:
xiaoyeliu
2026-01-24 10:43:54 +08:00
parent b1fb3e3289
commit b8bf1a8fe1
28 changed files with 23 additions and 6097 deletions
File diff suppressed because it is too large Load Diff
-521
View File
@@ -1,521 +0,0 @@
# OrcaSlicer G-code边界检测 - 最终实施报告
**项目编号**: ORCA-2026-001-FINAL
**实施日期**: 2026-01-20
**状态**: ✅ **全部完成**
---
## 📋 完成情况总览
| 漏洞ID | 描述 | 优先级 | 状态 | 位置 |
|--------|------|--------|------|------|
| #1 | 螺旋抬升边界检查 | P1 | ✅ 完成 | GCodeWriter.cpp:557-620 |
| #2 | 懒惰抬升边界检查 | P1 | ✅ 完成 | GCodeWriter.cpp:621-666 |
| #3 | 擦料塔位置验证 | P0 | ✅ 完成 | Print.cpp:1290-1327 |
| #4 | Skirt边界验证 | P1 | ✅ 完成 | Print.cpp:2385-2502 |
| #5 | Brim边界验证 | P1 | ✅ 完成 | Brim.cpp:1745-1800 |
| #6 | 支撑材料边界验证 | P2 | ✅ 完成 | SupportMaterial.cpp:587-662 |
| #7 | Travel移动验证 | P0 | ✅ 完成 | GCodeViewer.cpp:2403-2450 |
| #8 | 弧线路径验证(G2/G3) | P2 | ✅ 完成 | Python工具 |
**完成度**: 8/8 (100%)
---
## 📂 修改文件清单
### 新增文件 (3个)
| 文件 | 行数 | 说明 |
|------|------|------|
| `src/libslic3r/BoundaryValidator.hpp` | 149 | 边界验证器抽象接口 |
| `src/libslic3r/BoundaryValidator.cpp` | 211 | 边界验证器实现 |
| `tools/analyze_gcode_bounds.py` | ~500 | 命令行G-code检查工具 |
| `tools/gcode_boundary_checker_gui.py` | ~700 | GUI版G-code检查工具 |
### 修改文件 (9个)
| 文件 | 修改类型 | 主要变更 |
|------|----------|----------|
| `src/libslic3r/BuildVolume.hpp` | (无变更) | 保持原有接口 |
| `src/libslic3r/BuildVolume.cpp` | (无变更) | 保持原有实现 |
| `src/libslic3r/GCode/GCodeProcessor.hpp` | 结构扩展 | 扩展 `ConflictResult` |
| `src/libslic3r/Print.hpp` | 功能增强 | 添加边界超限追踪 |
| `src/libslic3r/Print.cpp` | 验证增强 | 擦料塔+Skirt边界检查 |
| `src/libslic3r/GCodeWriter.cpp` | 安全增强 | 螺旋/懒惰抬升边界检查与降级 |
| `src/libslic3r/Brim.cpp` | 验证增强 | Brim边界检查 |
| `src/libslic3r/Support/SupportMaterial.cpp` | 验证增强 | 支撑材料边界检查 |
| `src/slic3r/GUI/GCodeViewer.cpp` | 验证增强 | Travel移动边界检查 |
| `src/libslic3r/CMakeLists.txt` | 构建配置 | 添加新文件到构建 |
---
## 🎯 各模块实现详情
### 1. 边界验证框架 (BoundaryValidator)
**位置**: `src/libslic3r/BoundaryValidator.{hpp,cpp}`
**功能**:
- ✅ 点验证 (`validate_point`)
- ✅ 线段验证 (`validate_line`) - 沿线采样10点
- ✅ 弧线验证 (`validate_arc`) - 沿弧采样16点
- ✅ 多边形验证 (`validate_polygon`) - 检查所有顶点
**支持的床类型**:
- Rectangle (矩形床)
- Circle (圆形床/Delta)
- Convex (凸多边形床)
- Custom (自定义床)
**ViolationType 枚举**:
```cpp
enum class ViolationType {
SpiralLiftOutOfBounds,
LazyLiftOutOfBounds,
WipeTowerOutOfBounds,
SkirtOutOfBounds,
BrimOutOfBounds,
SupportOutOfBounds,
TravelMoveOutOfBounds,
ArcPathOutOfBounds
};
```
---
### 2. Travel移动边界检查 (漏洞#7)
**位置**: `src/slic3r/GUI/GCodeViewer.cpp:2427-2477`
**实现方式**: 内联检查(不使用BuildVolume函数)
**实现逻辑**:
```cpp
// 智能过滤:跳过初始化阶段
// 1. 找到第一个挤出移动 (Z > 0.1mm)
// 2. 只检查此之后的Travel移动
// 3. 使用 BedEpsilon 容差
// 4. 直接在检查循环中收集 BoundaryViolationInfo
```
**为什么不用独立的 BuildVolume 函数**:
- 需要收集详细的违规信息(类型、方向、位置、距离)
- 简单的布尔返回值无法提供足够的诊断数据
- 内联方式可以直接填充 `BoundaryViolationInfo` 结构
**关键特性**:
- ✅ 跳过G28/G29等初始化命令
- ✅ 只检查Travel移动 (Extrude已有检查)
- ✅ 确定超限方向 (X_min/X_max/Y_min/Y_max)
- ✅ 记录位置、距离和Z高度
- ✅ 填充到 `boundary_violations` 向量
---
### 3. 擦料塔位置验证 (漏洞#3)
**位置**: `src/libslic3r/Print.cpp:1290-1327`
**实现逻辑**:
```cpp
// 切片前验证擦料塔位置
// 1. 计算擦料塔实际占用的四个角 (包括brim)
// 2. 检查是否在床边界内
// 3. 如果超出,抛出阻断性错误
```
**验证内容**:
- 擦料塔基础尺寸 (width × depth)
- 包含 brim 的总尺寸
- 考虑板原点偏移
- 四个角落全检查
**错误类型**: 阻断性错误(禁止切片继续)
---
### 4. 螺旋/懒惰抬升边界检查 (漏洞#1, #2)
**位置**: `src/libslic3r/GCodeWriter.cpp:557-666`
**实现逻辑**:
```cpp
// 自动降级策略
if (m_to_lift_type == LiftType::SpiralLift) {
radius = delta_z / (2 * PI * atan(travel_slope));
if (radius > MAX_SAFE_SPIRAL_RADIUS) { // 50mm
// 降级为 Lazy Lift
BOOST_LOG_TRIVIAL(warning) << "Spiral lift radius too large, downgrading";
m_to_lift_type = LiftType::LazyLift;
}
}
if (m_to_lift_type == LiftType::LazyLift) {
slope_distance = delta_z / tan(travel_slope);
if (slope_distance > MAX_SAFE_SLOPE_DISTANCE) { // 100mm
// 降级为 Normal Lift
BOOST_LOG_TRIVIAL(warning) << "Lazy lift slope too long, downgrading";
m_to_lift_type = LiftType::NormalLift;
}
}
```
**降级链条**: SpiralLift → LazyLift → NormalLift
**安全阈值**:
- 螺旋抬升最大半径: 50mm
- 懒惰抬升最大斜坡距离: 100mm
---
### 5. Skirt边界验证 (漏洞#4)
**位置**: `src/libslic3r/Print.cpp:2385-2502`
**实现逻辑**:
```cpp
// 在生成每个Skirt loop后验证
for (size_t i = m_config.skirt_loops; i > 0; --i) {
// 生成Skirt loop
Polygon loop = offset(convex_hull, distance, ...);
// 验证边界
if (!validator.validate_polygon(loop, initial_layer_print_height)) {
// 记录超限但继续(不阻断)
this->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Skirt loop exceeds boundaries";
}
m_skirt.append(eloop);
}
```
**覆盖范围**:
- ✅ stCombined (统一Skirt)
- ✅ stPerObject (每个物体独立的Skirt)
**处理方式**: 记录警告但继续执行
---
### 6. Brim边界验证 (漏洞#5)
**位置**: `src/libslic3r/Brim.cpp:1745-1800`
**实现逻辑**:
```cpp
// 为每个物体验证Brim区域
for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) {
for (const ExPolygon& expoly : iter->second) {
if (!validator.validate_polygon(expoly.contour, first_layer_height)) {
// 记录超限
print_ptr->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Brim for object " << obj_name
<< " exceeds build volume boundaries";
}
}
}
```
**验证内容**:
- 物体Brim
- 支撑Brim
**处理方式**: 记录警告但继续执行
---
### 7. 支撑材料边界验证 (漏洞#6)
**位置**: `src/libslic3r/Support/SupportMaterial.cpp:587-662`
**实现逻辑**:
```cpp
// 在支撑生成完成后验证
for (const SupportLayer* layer : object.support_layers()) {
// 检查支撑挤出路径
for (const ExtrusionEntity* entity : layer->support_fills.entities) {
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(entity)) {
if (!validator.validate_polygon(path->polyline, layer->print_z)) {
support_violations++;
}
}
}
// 检查支撑多边形
for (const ExPolygon& expoly : layer->lslices) {
if (!validator.validate_polygon(expoly.contour, layer->print_z)) {
support_violations++;
}
}
}
```
**验证内容**:
- 支撑挤出路径 (ExtrusionPath)
- 支撑循环 (ExtrusionLoop)
- 支撑多边形 (ExPolygon)
- 支撑孔洞多边形
**处理方式**: 记录警告但继续执行
---
### 8. G2/G3弧线路径验证 (漏洞#8)
**位置**: Python工具 (`tools/analyze_gcode_bounds.py`, `tools/gcode_boundary_checker_gui.py`)
**实现逻辑**:
```python
def _parse_arc(self, line_num, line, code_part, g_code, ...):
# 解析弧线参数
i = float(i_match.group(1)) if i_match else 0.0 # X方向偏移
j = float(j_match.group(1)) if j_match else 0.0 # Y方向偏移
# 计算圆心和半径
center_x = start_x + i
center_y = start_y + j
radius = sqrt(i*i + j*j)
# 计算起始和结束角度
start_angle = atan2(start_y - center_y, start_x - center_x)
end_angle = atan2(end_y - center_y, end_x - center_x)
# 沿弧线采样检查 (至少8点,或每5mm一个点)
num_samples = max(8, int(abs(angle_sweep) * radius / 5))
for n in range(num_samples + 1):
# 计算采样点位置
sample_x = center_x + radius * cos(angle)
sample_y = center_y + radius * sin(angle)
# 检查此点是否在边界内
if not self._check_bounds(sample_pos):
# 记录超限
```
**支持功能**:
- ✅ G2 顺时针弧线
- ✅ G3 逆时针弧线
- ✅ 完整圆弧 (无X/Y参数)
- ✅ 部分圆弧 (有X/Y参数)
- ✅ Z轴插值
- ✅ 沿弧线多点采样
---
## 🔧 工具和辅助功能
### G-code边界检查工具
**GUI版本**: `tools/gcode_boundary_checker_gui.py`
- 图形界面操作
- 文件浏览器选择G-code
- 快速预设常见床尺寸
- 实时进度显示
- 详细报告生成
**命令行版本**: `tools/analyze_gcode_bounds.py`
- 适合脚本集成
- 批量处理
- 支持所有床类型
**功能特性**:
- ✅ 检测Travel移动超限
- ✅ 检测Extrude移动超限
- ✅ 检测G2/G3弧线超限
- ✅ 跳过纯Z移动 (避免误报)
- ✅ 按类型分类统计
- ✅ 详细位置信息
---
## 📊 技术细节
### 容差设置
| 用途 | 容差值 | 说明 |
|------|--------|------|
| BedEpsilon | 3×EPSILON ≈ 3e-5 mm | 原始精度 |
| Travel检查 | BedEpsilon | 与原有检查一致 |
| Python工具 | 0.01 mm | 10微米精度 |
| 螺旋抬升半径限制 | 50 mm | 安全阈值 |
| 懒惰抬升距离限制 | 100 mm | 安全阈值 |
### 性能考虑
| 功能 | 性能影响 | 说明 |
|------|----------|------|
| Travel检查 | < 2% | 仅在G-code预览时执行 |
| 擦料塔检查 | < 0.1% | 切片前一次性检查 |
| Skirt检查 | < 1% | 生成时并行检查 |
| Brim检查 | < 1% | 生成时并行检查 |
| 支撑检查 | < 2% | 生成完成后检查 |
### 内存使用
- BoundaryValidator: 轻量级,仅持有BuildVolume引用
- 违规记录: 每个违规约100字节
- 预期影响: 对于典型切片 < 1MB
---
## 🎨 设计模式
### 1. 策略模式
```cpp
// 抽象验证接口
class BoundaryValidator {
virtual bool validate_point(const Vec3d& point) const = 0;
virtual bool validate_line(const Vec3d& from, const Vec3d& to) const = 0;
// ...
};
// 具体实现
class BuildVolumeBoundaryValidator : public BoundaryValidator {
// 使用BuildVolume进行实际验证
};
```
### 2. 责任链模式
```cpp
// 抬升类型降级链
SpiralLift () LazyLift () NormalLift
```
### 3. 观察者模式
```cpp
// 记录违规到Print对象
print->add_boundary_violation(violation);
// GUI可监听并显示
```
---
## 🧪 测试建议
### 单元测试
**BoundaryValidator测试**:
```cpp
TEST_CASE("BoundaryValidator - Rectangle bed") {
std::vector<Vec2d> bed_shape = {{0,0}, {200,0}, {200,200}, {0,200}};
BuildVolume bv(bed_shape, 250.0);
BuildVolumeBoundaryValidator validator(bv);
REQUIRE(validator.validate_point(Vec3d(100, 100, 125))); // 内部
REQUIRE_FALSE(validator.validate_point(Vec3d(250, 100, 125))); // 超出
}
```
### 集成测试场景
| 场景 | 预期结果 | 优先级 |
|------|----------|--------|
| 标准正方体 | ✅ 无超限 | P0 |
| 大物体+Skirt | ⚠️ Skirt超限警告 | P1 |
| 擦料塔在床外 | ❌ 阻断性错误 | P0 |
| 螺旋抬升超限 | ⚠️ 降级+警告 | P1 |
| Travel移动超限 | ⚠️ 警告 | P0 |
| G2/G3弧线超限 | ⚠️ 检测并警告 | P2 |
| 支撑超限 | ⚠️ 警告 | P2 |
---
## 📈 改进效果
### 修复前 vs 修复后
| 场景 | 修复前 | 修复后 |
|------|--------|--------|
| Travel移动超限 | ❌ 不检查 | ✅ 检测并警告 |
| 擦料塔位置错误 | ❌ 不检查 | ✅ 切片前阻断 |
| 螺旋抬升超限 | ❌ 可能撞机 | ✅ 自动降级 |
| Skirt/Brim超限 | ❌ 不检查 | ✅ 记录警告 |
| 支撑超限 | ❌ 不检查 | ✅ 记录警告 |
| G2/G3弧线超限 | ❌ 不检查 | ✅ 检测并警告 |
### 用户影响
**安全性提升**:
- ✅ 防止打印头撞击边界
- ✅ 防止擦料塔超出范围
- ✅ 自动降级危险抬升
**可维护性提升**:
- ✅ 统一的验证框架
- ✅ 清晰的违规报告
- ✅ 详细的日志输出
**开发体验**:
- ✅ 可扩展的架构
- ✅ 易于添加新验证
- ✅ 完善的工具支持
---
## 🔮 后续优化建议
### 短期 (可选)
1. **配置选项**
```cpp
ConfigOptionBool strict_boundary_check {"strict_boundary_check", false};
ConfigOptionFloat boundary_check_epsilon {"boundary_check_epsilon", 0.0};
```
2. **GUI可视化**
- 在3D预览中高亮超限路径
- 显示违规位置标记
3. **更多测试**
- 扩展单元测试覆盖率
- 添加回归测试
### 长期 (可选)
1. **智能调整**
- 自动调整Skirt距离避免超限
- 自动调整Brim宽度
2. **预测性检查**
- 切片前预判是否会超限
- 提供调整建议
---
## 📝 总结
### 核心成就
**8个漏洞全部修复** - 100%完成
**系统性防御** - 多层边界检查
**自动化降级** - 智能处理临界情况
**完善工具** - Python诊断工具
### 代码质量
- ✅ 遵循现有代码风格
- ✅ 详细的注释和文档
- ✅ 清晰的错误消息
- ✅ 向后兼容
### 交付物
**代码文件**: 12个文件修改/新增
**文档文件**: 3个Markdown文档
**工具脚本**: 2个Python工具
**总计**: ~2000行新增/修改代码
---
**项目状态**: ✅ **完成并可交付**
**最后更新**: 2026-01-20
**版本**: v1.0-FINAL
@@ -1,772 +0,0 @@
# OrcaSlicer G-code边界检测优化实施报告
**项目编号**: ORCA-2026-001-IMPL
**实施日期**: 2026-01-16
**实施者**: Claude Code
**状态**: ⚠️ **已过时 - 中间实现文档**
> **重要说明**:本文档描述的是中间实现状态。最终实现与本文档有重要差异:
> - `BuildVolume::all_moves_inside()` 方法已在后来被**删除**
> - Travel 检查改为**内联实现**在 `GCodeViewer.cpp:2427-2477`
> - 参见 `gcode_boundary_final_implementation.md` 了解最终实现状态
> - 参见 `gcode_boundary_checking_optimization.md` 了解设计文档(已更新实际实现说明)
---
## 目录
1. [实施概述](#1-实施概述)
2. [修改文件清单](#2-修改文件清单)
3. [详细修改说明](#3-详细修改说明)
4. [测试建议](#4-测试建议)
5. [后续工作](#5-后续工作)
---
## 1. 实施概述
### 1.1 实施目标
根据技术文档 `gcode_boundary_checking_optimization.md` 中识别的8个关键漏洞,本次实施完成了以下核心修复:
**Phase 1: 基础设施** (已完成)
- 创建 BoundaryValidator 抽象验证框架
- 扩展 ConflictResult 支持边界超限类型
- 在 Print 类中添加边界超限追踪
**Phase 2: P0 关键修复** (已完成)
- 修复漏洞 #7: Travel Moves 验证缺失
- 修复漏洞 #3: 擦料塔位置验证缺失
**Phase 3: P1 高优先级修复** (已完成)
- 修复漏洞 #1: 螺旋抬升边界检查
- 修复漏洞 #2: 懒惰抬升边界检查
### 1.2 实施策略
采用**分层防御**策略:
1. **预防层**: 在路径生成时添加边界检查和自动降级
2. **检测层**: 在 G-code 生成后验证所有移动(包括 Travel)
3. **验证层**: 在切片前验证关键组件(如擦料塔)位置
---
## 2. 修改文件清单
### 2.1 新增文件
| 文件路径 | 行数 | 说明 |
|---------|------|------|
| `src/libslic3r/BoundaryValidator.hpp` | 149 | 边界验证器抽象接口和实现类 |
| `src/libslic3r/BoundaryValidator.cpp` | 211 | 边界验证器实现代码 |
| `docs/gcode_boundary_optimization_implementation.md` | - | 本实施报告 |
**总计新增代码**: ~360 行
### 2.2 修改文件
| 文件路径 | 修改类型 | 行数变化 | 说明 |
|---------|----------|----------|------|
| `src/libslic3r/BuildVolume.hpp` | 功能增强 | +3 | 新增 `all_moves_inside()` 方法声明 |
| `src/libslic3r/BuildVolume.cpp` | 功能增强 | +52 | 实现 `all_moves_inside()` 验证所有移动 |
| `src/libslic3r/GCode/GCodeProcessor.hpp` | 结构扩展 | +60 | 扩展 `ConflictResult` 支持边界超限 |
| `src/libslic3r/Print.hpp` | 功能增强 | +20 | 添加边界超限追踪方法 |
| `src/libslic3r/Print.cpp` | 验证增强 | +35 | 在 `validate()` 中添加擦料塔边界检查 |
| `src/libslic3r/GCodeWriter.cpp` | 安全增强 | +60 | 螺旋/懒惰抬升边界检查与降级 |
| `src/slic3r/GUI/GCodeViewer.cpp` | 验证增强 | +10 | 调用 `all_moves_inside()` 检测 Travel 移动 |
| `src/libslic3r/CMakeLists.txt` | 构建配置 | +2 | 添加 BoundaryValidator 到构建列表 |
**总计修改**: 8个文件,~242 行新增/修改
---
## 3. 详细修改说明
### 3.1 Phase 1: 基础设施建设
#### 3.1.1 创建 BoundaryValidator 框架
**文件**: `src/libslic3r/BoundaryValidator.hpp`
**设计理念**:
- 提供统一的边界验证接口,支持点、线、弧、多边形验证
- 使用抽象基类设计,便于未来扩展不同验证策略
- 基于 BuildVolume 的具体实现支持所有打印床类型
**核心接口**:
```cpp
class BoundaryValidator {
public:
enum class ViolationType {
SpiralLiftOutOfBounds, // 螺旋抬升超限
LazyLiftOutOfBounds, // 懒惰抬升超限
WipeTowerOutOfBounds, // 擦料塔超限
SkirtOutOfBounds, // 裙边超限
BrimOutOfBounds, // Brim 超限
SupportOutOfBounds, // 支撑超限
TravelMoveOutOfBounds, // Travel 移动超限
ArcPathOutOfBounds // 弧线路径超限
};
virtual bool validate_point(const Vec3d& point) const = 0;
virtual bool validate_line(const Vec3d& from, const Vec3d& to) const = 0;
virtual bool validate_arc(...) const = 0;
virtual bool validate_polygon(...) const = 0;
};
```
**实现要点**:
1. **点验证**: 检查 XY 坐标和 Z 高度
2. **线段验证**: 沿线段采样10个点验证
3. **弧线验证**: 沿弧线采样16个点验证(防止弧线中段超限)
4. **多边形验证**: 检查所有顶点
**支持的打印床类型**:
- Rectangle (矩形) - 使用 BoundingBox 检测
- Circle (圆形) - 使用距离平方检测
- Convex/Custom (凸/自定义) - 使用点在多边形内检测
**代码位置**: `BoundaryValidator.cpp:47-117`
---
#### 3.1.2 扩展 ConflictResult 结构
**文件**: `src/libslic3r/GCode/GCodeProcessor.hpp`
**修改原因**:
- 原有 `ConflictResult` 只支持对象间冲突
- 需要扩展以支持边界超限类型
**新增字段**:
```cpp
struct ConflictResult {
// 原有字段
std::string _objName1, _objName2;
double _height;
const void *_obj1, *_obj2;
int layer;
// 新增字段
enum class ConflictType {
ObjectCollision, // 原有: 对象间冲突
BoundaryViolation // 新增: 边界超限
};
ConflictType conflict_type = ConflictType::ObjectCollision;
int violation_type_int = -1; // 存储 ViolationType
Vec3d violation_position; // 超限位置
// 新增静态工厂方法
static ConflictResult create_boundary_violation(...);
// 新增辅助方法
bool is_boundary_violation() const;
bool is_object_collision() const;
};
```
**设计考虑**:
- 保持向后兼容:默认构造仍为 `ObjectCollision`
- 使用 `int` 存储枚举避免跨模块依赖问题
- 提供类型检查辅助方法
**代码位置**: `GCodeProcessor.hpp:110-167`
---
#### 3.1.3 在 Print 类添加边界超限追踪
**文件**: `src/libslic3r/Print.hpp`, `src/libslic3r/Print.cpp`
**新增成员变量**:
```cpp
class Print {
ConflictResultOpt m_conflict_result; // 原有
std::vector<ConflictResult> m_boundary_violations; // 新增
};
```
**新增方法**:
```cpp
void add_boundary_violation(const ConflictResult& violation);
const std::vector<ConflictResult>& get_boundary_violations() const;
void clear_boundary_violations();
bool has_boundary_violations() const;
```
**用途**:
- 收集切片过程中发现的所有边界超限
- 供 GUI 显示警告和可视化
- 支持批量检测和报告
**代码位置**:
- 声明: `Print.hpp:973-988`
- 定义: `Print.hpp:1065` (成员变量)
---
### 3.2 Phase 2: P0 关键修复
#### 3.2.1 修复漏洞 #7: Travel Moves 验证缺失 ⭐⭐⭐⭐⭐
**问题描述**:
- 原有 `all_paths_inside()` 只验证挤出移动,忽略 Travel 移动
- Travel 移动超出边界可能导致打印头撞击
**修复方案**:
**1) 新增 `BuildVolume::all_moves_inside()` 方法**
**文件**: `src/libslic3r/BuildVolume.hpp`, `BuildVolume.cpp`
**原有代码逻辑**:
```cpp
// BuildVolume.cpp:330 - 原有的 all_paths_inside()
auto move_valid = [](const GCodeProcessorResult::MoveVertex &move) {
return move.type == EMoveType::Extrude && // 只检查挤出!
move.extrusion_role != erCustom &&
move.width != 0.f &&
move.height != 0.f;
};
```
**新增代码逻辑**:
```cpp
// BuildVolume.cpp:371 - 新增的 all_moves_inside()
auto move_significant = [](const GCodeProcessorResult::MoveVertex &move) {
return move.type == EMoveType::Extrude ||
move.type == EMoveType::Travel; // 同时检查 Travel!
};
```
**实现细节**:
- 验证所有 `Extrude``Travel` 类型移动
- 排除 `Retract``Unretract`Z轴不移动)
- 支持 Rectangle, Circle, Convex, Custom 所有打印床类型
- 逐点验证每个移动的终点位置
**2) 在 GCodeViewer 中调用验证**
**文件**: `src/slic3r/GUI/GCodeViewer.cpp`
**修改位置**: 行 2398-2433
**调用逻辑**:
```cpp
// 先检查挤出路径(原有)
m_contained_in_bed = build_volume.all_paths_inside(gcode_result, m_paths_bounding_box);
// 新增: 同时检查 Travel 移动
if (m_contained_in_bed) {
bool all_moves_valid = build_volume.all_moves_inside(gcode_result, m_paths_bounding_box);
if (!all_moves_valid) {
m_contained_in_bed = false;
BOOST_LOG_TRIVIAL(warning) << "Travel moves detected outside build volume boundaries";
}
}
```
**效果**:
- ✅ 检测所有 Travel 移动超限
- ✅ 设置 `toolpath_outside` 标志触发 GUI 警告
- ✅ 防止打印头撞击边界
**影响范围**: **所有打印**(系统性修复)
**代码位置**:
- 方法声明: `BuildVolume.hpp:96`
- 方法实现: `BuildVolume.cpp:371-419`
- 调用点: `GCodeViewer.cpp:2405-2411`
---
#### 3.2.2 修复漏洞 #3: 擦料塔位置验证缺失 ⭐⭐⭐⭐⭐
**问题描述**:
- 擦料塔(Prime Tower)位置由用户手动设置
- 原代码只检查与其他对象的碰撞,不检查是否超出床边界
- 包括 brim 的实际占用面积可能远大于配置宽度
**修复方案**:
**文件**: `src/libslic3r/Print.cpp`
**修改位置**: `Print::validate()` 方法,行 1289-1323
**实现代码**:
```cpp
// 在擦料塔验证段末尾添加(has_wipe_tower() 块内)
{
const size_t plate_index = this->get_plate_index();
const Vec3d plate_origin = this->get_plate_origin();
const float x = m_config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
const float y = m_config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
const float width = m_config.prime_tower_width.value;
const float brim_width = m_config.prime_tower_brim_width.value;
const float depth = this->wipe_tower_data(extruders.size()).depth;
// 创建床边界框
BoundingBoxf bed_bbox;
for (const Vec2d& pt : m_config.printable_area.values) {
bed_bbox.merge(pt);
}
bool tower_outside = false;
// 检查所有四个角(包括 brim)
if (x - brim_width < bed_bbox.min.x() ||
x + width + brim_width > bed_bbox.max.x() ||
y - brim_width < bed_bbox.min.y() ||
y + depth + brim_width > bed_bbox.max.y()) {
tower_outside = true;
}
if (tower_outside) {
const float total_width = width + 2 * brim_width;
const float total_depth = depth + 2 * brim_width;
return StringObjectException{
Slic3r::format(_u8L("The prime tower at position (%.2f, %.2f) "
"with dimensions %.2f x %.2f mm "
"(including %.2f mm brim) exceeds the bed boundaries. "
"Please adjust the prime tower position in the configuration."),
x, y, total_width, total_depth, brim_width),
nullptr,
"wipe_tower_x"
};
}
}
```
**验证内容**:
- ✅ 擦料塔基础尺寸 (width × depth)
- ✅ 包含 brim 的总尺寸 (width + 2×brim_width) × (depth + 2×brim_width)
- ✅ 四个角落是否在床边界内
- ✅ 考虑板原点偏移 (plate_origin)
**错误类型**: **阻断性错误**
- 不允许切片继续
- 用户必须调整擦料塔位置
- 提供清晰的错误信息和修复建议
**效果**:
- ✅ 防止擦料塔超出边界导致撞机
- ✅ 提前发现问题,避免打印失败
- ✅ 提供详细的错误位置和尺寸信息
**影响范围**: 所有使用擦料塔的多材料打印
**代码位置**: `Print.cpp:1289-1323`
---
### 3.3 Phase 3: P1 高优先级修复
#### 3.3.1 修复漏洞 #1 & #2: 螺旋/懒惰抬升边界检查 ⭐⭐⭐⭐
**问题描述**:
**漏洞 #1 - 螺旋抬升 (Spiral Lift)**:
- 使用 G2/G3 弧线命令抬升 Z 轴
- 弧线半径计算: `radius = delta_z / (2π × tan(slope))`
- 大 Z 抬升 → 大半径 → 可能超出边界
- 原代码有 TODO 注释但未实现
**漏洞 #2 - 懒惰抬升 (Lazy Lift)**:
- 沿斜坡移动抬升 Z 轴
- 斜坡距离计算: `distance = delta_z / tan(slope)`
- 长距离移动 → 大斜坡延伸 → 可能超出边界
**修复方案**: 自动降级策略
**文件**: `src/libslic3r/GCodeWriter.cpp`
**修改位置**: `GCodeWriter::travel_to_xyz()` 方法,行 543-602
**实现逻辑**:
```cpp
if (delta(2) > 0 && delta_no_z.norm() != 0.0f) {
// 螺旋抬升检查
if (m_to_lift_type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = delta(2) / (2 * PI * atan(this->extruder()->travel_slope()));
constexpr double MAX_SAFE_SPIRAL_RADIUS = 50.0; // mm
if (radius > MAX_SAFE_SPIRAL_RADIUS) {
BOOST_LOG_TRIVIAL(warning) << "Spiral lift radius (" << radius
<< " mm) exceeds safe limit, downgrading to lazy lift";
m_to_lift_type = LiftType::LazyLift; // 降级
}
else {
// 执行螺旋抬升
Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) };
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
}
}
// 懒惰抬升检查
if (m_to_lift_type == LiftType::LazyLift &&
this->is_current_position_clear() &&
atan2(delta(2), delta_no_z.norm()) < this->extruder()->travel_slope()) {
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->extruder()->travel_slope());
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
constexpr double MAX_SAFE_SLOPE_DISTANCE = 100.0; // mm
double slope_distance = temp.norm();
if (slope_distance > MAX_SAFE_SLOPE_DISTANCE) {
BOOST_LOG_TRIVIAL(warning) << "Lazy lift slope distance (" << slope_distance
<< " mm) exceeds safe limit, downgrading to normal lift";
m_to_lift_type = LiftType::NormalLift; // 降级
}
else {
// 执行懒惰抬升
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
}
// 正常抬升(兜底)
if (m_to_lift_type == LiftType::NormalLift) {
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
```
**安全阈值设定**:
- **螺旋抬升**: 最大半径 50mm
- 典型200×200mm床: 对角线 ~282mm,半径50mm是安全的
- 超过此值可能接近床边缘
- **懒惰抬升**: 最大斜坡距离 100mm
- 大多数打印床尺寸下安全
- 防止极端长距离移动
**降级策略**:
1. SpiralLift → LazyLift → NormalLift
2. 逐级降级确保安全
3. 记录警告日志便于调试
**效果**:
- ✅ 自动检测并防止超限
- ✅ 保持功能可用性(降级而非禁用)
- ✅ 提供日志记录便于诊断
- ✅ 无需用户干预
**影响范围**: 使用螺旋/懒惰抬升的打印
**代码位置**: `GCodeWriter.cpp:545-602`
---
## 4. 测试建议
### 4.1 单元测试场景
#### 4.1.1 BoundaryValidator 测试
**测试文件**: `tests/libslic3r/test_boundary_validator.cpp` (建议创建)
**测试用例**:
```cpp
TEST_CASE("BoundaryValidator - Rectangle bed", "[boundary]") {
std::vector<Vec2d> bed_shape = {{0,0}, {200,0}, {200,200}, {0,200}};
BuildVolume bv(bed_shape, 250.0);
BuildVolumeBoundaryValidator validator(bv);
// 测试点验证
REQUIRE(validator.validate_point(Vec3d(100, 100, 125))); // 中心点
REQUIRE_FALSE(validator.validate_point(Vec3d(250, 100, 125))); // 超出X
REQUIRE_FALSE(validator.validate_point(Vec3d(100, 100, 300))); // 超出Z
// 测试线段验证
REQUIRE(validator.validate_line(Vec3d(50,50,10), Vec3d(150,150,10)));
REQUIRE_FALSE(validator.validate_line(Vec3d(50,50,10), Vec3d(250,250,10)));
// 测试弧线验证
// ...
}
TEST_CASE("BoundaryValidator - Circle bed", "[boundary]") {
// Delta 打印机测试
// ...
}
```
#### 4.1.2 Travel Moves 验证测试
**测试场景**:
```cpp
TEST_CASE("BuildVolume - all_moves_inside includes Travel", "[buildvolume]") {
// 创建包含 Travel 移动的 GCodeProcessorResult
GCodeProcessorResult result;
// 添加合法的 Travel 移动
result.moves.push_back({.type = EMoveType::Travel, .position = {100,100,50}});
REQUIRE(bv.all_moves_inside(result, bbox));
// 添加超限的 Travel 移动
result.moves.push_back({.type = EMoveType::Travel, .position = {250,100,50}});
REQUIRE_FALSE(bv.all_moves_inside(result, bbox));
}
```
### 4.2 集成测试场景
#### 场景 T1: 大物体 + 大 Skirt (P0)
- **设置**: 物体 195×195mm, Skirt 距离 10mm, 床 200×200mm
- **预期**: 警告 Skirt 超限(尚未实现此修复)
- **优先级**: P1
#### 场景 T2: 擦料塔在床外 (P0) ✅
- **设置**: 手动设置塔位置 (210, 210), 床 200×200mm
- **预期**: 阻断性错误,禁止切片
- **验证**: `Print::validate()` 返回错误
- **状态**: ✅ 已实现
#### 场景 T3: 螺旋抬升超限 (P1) ✅
- **设置**: 物体在 (195, 0), 启用 Spiral Lift, 大 Z 抬升
- **预期**: 自动降级为 Lazy Lift,日志警告
- **验证**: 检查 G-code 中无 G2/G3 命令
- **状态**: ✅ 已实现
#### 场景 T4: Travel 移动超限 (P0) ✅
- **设置**: 多物体,Travel 路径超出边界
- **预期**: `toolpath_outside` 标志设置,GUI 显示警告
- **验证**: GCodeViewer 显示橙色警告
- **状态**: ✅ 已实现
### 4.3 回归测试
**关键检查点**:
1. ✅ 正常打印不受影响(无误报)
2. ✅ 性能影响 < 5% (边界检查开销)
3. ✅ 原有冲突检测功能正常工作
4. ✅ GUI 警告显示正确
### 4.4 性能测试
**测试方法**:
```bash
# 测试大型模型切片时间
# Before: xxx seconds
# After: xxx seconds (+X%)
```
**预期性能影响**:
- `all_moves_inside()`: +1-2% (逐点检查)
- 擦料塔验证: +0.1% (切片前一次性检查)
- 抬升降级: 0% (仅在触发时)
---
## 5. 后续工作
### 5.1 未完成的 P1/P2 修复
根据原技术文档,以下漏洞尚未修复:
#### 漏洞 #4: Skirt 超限 (P1) ⏳
**位置**: `src/libslic3r/Print.cpp:2338-2357`
**修复方案**: 在 Skirt 生成后添加边界验证
**优先级**: 高
#### 漏洞 #5: Brim 超限 (P1) ⏳
**位置**: `src/libslic3r/Brim.cpp`
**修复方案**: 在 Brim 生成后添加边界验证
**优先级**: 高
#### 漏洞 #6: 支撑材料超限 (P2) ⏳
**位置**: `src/libslic3r/SupportMaterial.cpp`, `src/libslic3r/Support/TreeSupport.cpp`
**修复方案**: 在支撑生成时限制边界
**优先级**: 中
#### 漏洞 #8: 弧线路径超限 (P2) ⏳
**位置**: `src/libslic3r/GCodeWriter.cpp:673-691, 732-752`
**修复方案**: 在 `_spiral_travel_to_z()``extrude_arc_to_xy()` 中使用 `validate_arc()`
**优先级**: 中
### 5.2 GUI 增强
#### 5.2.1 可视化边界超限 ⏳
- 在 3D 预览中高亮显示超限路径
- 使用红色标记超限的 Travel 移动
- 显示擦料塔边界框
#### 5.2.2 警告消息改进 ⏳
- 扩展 `GLCanvas3D::EWarning` 枚举
- 添加边界超限专用警告类型
- 提供详细的超限位置信息
### 5.3 配置选项 ⏳
建议添加高级配置:
```cpp
// PrintConfig 中添加
class PrintConfig {
ConfigOptionBool strict_boundary_check {"strict_boundary_check", false};
ConfigOptionFloat boundary_check_epsilon {"boundary_check_epsilon", 3.0};
};
```
**用途**:
- `strict_boundary_check`: 将警告升级为错误
- `boundary_check_epsilon`: 调整边界容差
### 5.4 文档和测试 ⏳
- [ ] 完善单元测试覆盖率至 >85%
- [ ] 创建集成测试套件
- [ ] 编写用户文档说明新警告
- [ ] 更新开发者文档
---
## 6. 总结
### 6.1 完成情况
| 阶段 | 内容 | 状态 | 完成度 |
|------|------|------|--------|
| Phase 1 | 基础设施建设 | ✅ 完成 | 100% |
| Phase 2 | P0 关键修复 | ✅ 完成 | 100% |
| Phase 3 | P1 高优先级修复 (部分) | ✅ 完成 | 50% |
| Phase 4 | P2 修复 | ⏳ 未开始 | 0% |
| Phase 5 | GUI 增强 | ⏳ 未开始 | 0% |
| 总体 | - | 🟡 部分完成 | **60%** |
### 6.2 关键成果
**系统性修复**:
- Travel Moves 验证缺失(影响最广的漏洞)
- 擦料塔位置验证缺失(高风险漏洞)
**安全增强**:
- 螺旋/懒惰抬升自动降级机制
- 多层防御策略
**代码质量**:
- 新增 ~360 行高质量代码
- 修改/增强 ~242 行现有代码
- 编译通过,无警告
**可扩展性**:
- BoundaryValidator 框架便于未来扩展
- ConflictResult 扩展支持更多验证类型
### 6.3 风险评估
**技术风险**: 🟢 低
- 所有修改已编译通过
- 向后兼容现有功能
- 采用防御性编程策略
**性能风险**: 🟢 低
- 预期性能影响 < 5%
- 边界检查使用高效算法
- 仅在必要时触发验证
**兼容性风险**: 🟢 低
- 不影响现有 G-code 输出
- 仅增加验证和警告
- 不改变切片算法
### 6.4 建议后续步骤
**立即行动**:
1. ✅ 编译验证 - 已完成
2. 🔄 单元测试 - 进行中
3. 🔄 集成测试 - 待开始
**短期目标** (1-2周):
1. 完成 Skirt/Brim 边界验证 (P1)
2. 添加基础单元测试
3. 进行回归测试
**中期目标** (1个月):
1. 完成所有 P2 修复
2. GUI 可视化增强
3. 性能优化
---
## 附录
### A. 修改的代码行统计
```
新增文件:
BoundaryValidator.hpp 149 lines
BoundaryValidator.cpp 211 lines
实施文档 本文档
修改文件:
BuildVolume.hpp +3 lines
BuildVolume.cpp +52 lines
GCodeProcessor.hpp +60 lines
Print.hpp +20 lines
Print.cpp +35 lines
GCodeWriter.cpp +60 lines
GCodeViewer.cpp +10 lines
CMakeLists.txt +2 lines
总计: 新增 ~360 行, 修改 ~242 行
```
### B. 编译验证
```
编译器: MSVC 17.11 (Visual Studio 2022)
配置: Release x64
结果: ✅ 成功
警告: 0
错误: 0
```
### C. Git 提交建议
```bash
git add src/libslic3r/BoundaryValidator.*
git add src/libslic3r/BuildVolume.*
git add src/libslic3r/Print.*
git add src/libslic3r/GCode/GCodeProcessor.hpp
git add src/libslic3r/GCodeWriter.cpp
git add src/slic3r/GUI/GCodeViewer.cpp
git add src/libslic3r/CMakeLists.txt
git add docs/gcode_boundary_optimization_implementation.md
git commit -m "feat: Implement G-code boundary checking optimizations
Fixes critical vulnerabilities in boundary validation:
- ✅ P0: Add Travel moves validation (system-wide fix)
- ✅ P0: Add wipe tower position validation (blocking error)
- ✅ P1: Add spiral/lazy lift boundary check with auto-downgrade
- ✅ Infrastructure: Create BoundaryValidator framework
- ✅ Infrastructure: Extend ConflictResult for boundary violations
Details:
- New files: BoundaryValidator.hpp/cpp (~360 lines)
- Modified: 8 files (~242 lines)
- Compilation: ✅ Passed with no warnings
- Performance impact: < 5% expected
Related: ORCA-2026-001
Documentation: docs/gcode_boundary_optimization_implementation.md
"
```
---
**文档结束**
**实施者**: Claude Code
**审核**: 待用户审核
**版本**: v1.0
**日期**: 2026-01-16
-619
View File
@@ -1,619 +0,0 @@
# OrcaSlicer G-code边界检测 - 发版风险评估与测试指南
**文档版本**: v1.0-RISK
**创建日期**: 2026-01-20
**项目编号**: ORCA-2026-001-RELEASE
**风险等级**: 🟡 **中等风险** (需要充分测试)
---
## 📋 执行摘要
### 核心变更
本次实施为OrcaSlicer添加了**完整的边界检测系统**,包括8个漏洞修复,共涉及12个文件,约2000行新增/修改代码。
### 风险评级
| 维度 | 风险等级 | 说明 |
|------|----------|------|
| **功能影响** | 🟡 中 | 改变边界检查行为,可能影响部分切片结果 |
| **性能影响** | 🟢 低 | 性能影响 < 5%,用户无感知 |
| **兼容性** | 🟡 中 | 可能影响现有打印配置(边缘打印) |
| **回滚难度** | 🟢 低 | 修改集中,可快速回滚 |
| **测试覆盖** | 🟡 中 | 需要新增测试用例 |
### 建议措施
**推荐发布** - 建议在充分测试后发布
⚠️ **必须测试** - 边界打印场景需要验证
📝 **发布说明** - 需要在更新日志中说明变更
---
## 📂 修改文件清单
### 核心代码修改 (9个文件)
| 文件 | 修改类型 | 代码量 | 风险等级 | 说明 |
|------|----------|--------|----------|------|
| `src/libslic3r/BuildVolume.hpp` | 新增方法 | +3 | 🟢 低 | 新增接口声明 |
| `src/libslic3r/BuildVolume.cpp` | 新增方法 | +52 | 🟢 低 | Travel检查实现 |
| `src/libslic3r/GCode/GCodeProcessor.hpp` | 结构扩展 | +60 | 🟢 低 | 扩展ConflictResult |
| `src/libslic3r/Print.hpp` | 新增方法 | +20 | 🟢 低 | 违规追踪接口 |
| `src/libslic3r/Print.cpp` | 验证增强 | +70 | 🟡 **中** | 擦料塔+Skirt检查 |
| `src/libslic3r/GCodeWriter.cpp` | 逻辑增强 | +130 | 🟡 **中** | 螺旋/懒惰抬升降级 |
| `src/libslic3r/Brim.cpp` | 验证增强 | +60 | 🟡 **中** | Brim边界检查 |
| `src/libslic3r/Support/SupportMaterial.cpp` | 验证增强 | +80 | 🟡 **中** | 支撑边界检查 |
| `src/slic3r/GUI/GCodeViewer.cpp` | 验证增强 | +50 | 🟡 **中** | Travel移动检查 |
### 新增文件 (4个)
| 文件 | 行数 | 用途 | 风险 |
|------|------|------|------|
| `src/libslic3r/BoundaryValidator.hpp` | 149 | 验证框架接口 | 🟢 低 |
| `src/libslic3r/BoundaryValidator.cpp` | 211 | 验证框架实现 | 🟢 低 |
| `tools/analyze_gcode_bounds.py` | ~500 | 命令行诊断工具 | 🟢 无 |
| `tools/gcode_boundary_checker_gui.py` | ~700 | GUI诊断工具 | 🟢 无 |
**总计**: 12个文件,~2000行代码
---
## 🔍 功能变更详解
### 变更1: Travel移动边界检查 (影响:🟡 中)
**位置**: `src/slic3r/GUI/GCodeViewer.cpp:2403-2450`
**变更内容**:
```cpp
// 之前:只检查Extrude移动
m_contained_in_bed = build_volume.all_paths_inside(gcode_result, m_paths_bounding_box);
// 之后:同时检查Travel移动
if (m_contained_in_bed) {
bool all_moves_valid = build_volume.all_moves_inside(gcode_result, ...);
if (!all_moves_valid) {
m_contained_in_bed = false; // 设置超限标志
}
}
```
**影响范围**:
- ✅ 所有切片的G-code预览
- ✅ 会检测到之前被忽略的Travel移动超限
**风险场景**:
- ⚠️ 之前允许的边缘Travel移动现在会报错
- ⚠️ 可能影响:边缘擦料塔、边缘物体的大跨度移动
**用户可见变化**:
- G-code预览可能显示橙色"toolpath_outside"警告
- 右下角可能显示"部分路径超出打印床"提示
**缓解措施**:
- 智能过滤:跳过G28/G29初始化阶段
- 使用BedEpsilon容差(3e-5mm,极小)
- 只警告,不阻断切片
---
### 变更2: 擦料塔位置验证 (影响:🟡 高)
**位置**: `src/libslic3r/Print.cpp:1290-1327`
**变更内容**:
```cpp
// 之前:不验证擦料塔位置
// 之后:切片前严格验证
if () {
return StringObjectException{}; // 阻断切片
}
```
**影响范围**:
- ✅ 所有使用擦料塔的多材料打印
- ✅ 所有使用擦料塔的支撑/界面打印
**风险场景**:
-**阻断性**: 如果擦料塔位置设置在床外,切片会**完全失败**
- ⚠️ 用户之前可能设置了超出边界的擦料塔位置,现在无法切片
**用户可见变化**:
- 错误提示:"The prime tower at position (x, y) with dimensions W×D mm (including brim) exceeds the bed boundaries"
**缓解措施**:
- 错误信息清晰,提供具体位置和尺寸
- 建议用户调整擦料塔位置
---
### 变更3: 螺旋/懒惰抬升自动降级 (影响:🟡 中)
**位置**: `src/libslic3r/GCodeWriter.cpp:557-663`
**变更内容**:
```cpp
// 降级链条:
SpiralLift [线] LazyLift [] NormalLift
```
**影响范围**:
- ✅ 所有启用"螺旋抬升"的切片
- ✅ 所有启用"懒惰抬升"的切片
- ✅ 主要影响边缘区域的抬升行为
**风险场景**:
- ⚠️ 边缘区域的抬升方式可能改变
- ⚠️ 可能轻微影响打印质量(抬升方式不同)
- ✅ 但不会超限撞机
**用户可见变化**:
- 一般情况:无任何变化
- 边缘打印:日志中可能出现降级警告
- 极端情况:抬升路径改变(更安全)
**缓解措施**:
- 逐级降级,保持功能可用
- 详细日志记录
- 只在必要时降级
---
### 变更4: Skirt/Brim/支撑边界检查 (影响:🟡 中)
**位置**:
- Skirt: `Print.cpp:2385-2502`
- Brim: `Brim.cpp:1745-1800`
- 支撑: `SupportMaterial.cpp:587-662`
**变更内容**:
```cpp
// 之前:生成后不验证边界
// 之后:生成时验证并记录违规
if (!validator.validate_polygon(geometry, z_height)) {
print->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "... exceeds boundaries";
}
```
**影响范围**:
- ✅ 所有使用Skirt的打印
- ✅ 所有使用Brim的打印
- ✅ 所有使用支撑的打印
**风险场景**:
- ⚠️ 大尺寸Skirt/Brim可能被记录为违规
- ⚠️ 支撑超出边界会被记录
- ✅ 但不阻断切片,只记录警告
**用户可见变化**:
- 一般情况:无任何变化
- 违规情况:日志中有警告
- 未来版本可能显示警告UI
**缓解措施**:
- 非阻断性(不停止切片)
- 只记录违规,供后续分析
- 可通过配置调整Skirt/Brim参数避免
---
## ⚠️ 风险分析矩阵
### 高风险场景
| 场景 | 风险等级 | 概率 | 影响 | 缓解措施 |
|------|----------|------|------|----------|
| 擦料塔位置超出边界 | 🔴 高 | 中 | **无法切片** | 清晰错误信息,引导调整位置 |
| 边缘物体+螺旋抬升 | 🟡 中 | 高 | 抬升方式改变 | 自动降级,保持安全 |
| 大尺寸Skirt | 🟡 中 | 中 | 记录违规警告 | 非阻断,可调整参数 |
| 边缘物体的大跨度Travel | 🟡 中 | 低 | 可能报超限 | 智能过滤初始化阶段 |
### 低风险场景
| 场景 | 风险等级 | 说明 |
|------|----------|------|
| 标准物体(床中心) | 🟢 低 | 无影响 |
| 小物体 | 🟢 低 | 无影响 |
| 正常参数设置 | 🟢 低 | 无影响 |
---
## 🧪 测试用例设计
### P0 - 必须测试 (Critical)
#### TC01: 标准打印 - 无风险验证
**目的**: 确保正常打印不受影响
**步骤**:
1. 加载标准测试模型(如20mm立方体)
2. 放置在床中心位置
3. 使用默认参数切片
4. 验证:
- ✅ 切片成功,无错误
- ✅ 警告日志数量为0(或只有正常信息)
- ✅ G-code预览显示正常
- ✅ 打印时间无显著变化
**预期结果**: 完全正常,无任何影响
---
#### TC02: 擦料塔位置验证 - 阻断性测试
**目的**: 验证超出边界的擦料塔被正确阻止
**步骤**:
1. 创建双材料打印配置
2. **手动设置擦料塔位置在床外**(如 X=300, Y=300,对于200×200床)
3. 尝试切片
**预期结果**:
- ❌ 切片**失败**,显示错误:
```
The prime tower at position (300.00, 300.00) with dimensions
XX × XX mm exceeds the bed boundaries.
Please adjust the prime tower position.
```
**验证点**:
- ✅ 错误信息清晰
- ✅ 提供具体位置
- ✅ 切片被阻断
---
#### TC03: 边缘物体+螺旋抬升 - 降级验证
**目的**: 验证螺旋抬升自动降级机制
**步骤**:
1. 创建大物体(190×190mm,对于200×200床)
2. 启用**螺旋抬升**
3. 切片
**预期结果**:
- ✅ 切片成功
- ⚠️ 日志中出现降级警告:
```
Spiral lift arc exceeds build volume boundaries,
downgrading to lazy lift
```
```
Lazy lift slope exceeds build volume boundaries,
downgrading to normal lift
```
**验证点**:
- ✅ 自动降级生效
- ✅ 打印路径仍在床内
- ✅ 不影响切片完成
---
### P1 - 应该测试 (High)
#### TC04: Travel移动检测
**目的**: 验证Travel移动边界检查
**步骤**:
1. 创建两个物体,分开放置在床的对角
2. 确保它们之间的Travel路径会经过床边缘附近
3. 切片
4. 检查G-code预览
**预期结果**:
- ✅ 如果Travel在边界内:正常显示
- ⚠️ 如果Travel超限:橙色警告
---
#### TC05: Skirt边界检查
**目的**: 验证Skirt超出边界时记录警告
**步骤**:
1. 创建195×195mm物体(对于200×200床)
2. 设置Skirt距离为10mm
3. 切片
**预期结果**:
- ✅ 切片成功
- ⚠️ 日志中记录Skirt超限警告
- ✅ 不阻断切片
---
#### TC06: 支撑边界检查
**目的**: 验证支撑超出边界时记录警告
**步骤**:
1. 创建需要大量支撑的模型
2. 确保支撑可能延伸到床边缘
3. 切片
**预期结果**:
- ✅ 切片成功
- ⚠️ 如有超限,日志记录警告
- ✅ 不阻断切片
---
### P2 - 可选测试 (Medium)
#### TC07: G2/G3弧线命令验证
**目的**: 验证Python工具能检测弧线超限
**步骤**:
1. 生成包含G2/G3命令的G-code
2. 使用Python工具分析:
```bash
python analyze_gcode_bounds.py output.gcode --bed-size 200 200 250
```
**预期结果**:
- ✅ 能正确解析G2/G3
- ✅ 能检测弧线超限
---
#### TC08: 极限边界打印
**目的**: 验证紧贴边界的打印
**步骤**:
1. 创建199×199mm物体(对于200×200床)
2. 放置在床的角落
3. 启用所有功能(螺旋抬升、Skirt、Brim、支撑)
4. 切片
**预期结果**:
- ✅ 切片成功(功能降级生效)
- ⚠️ 可能有多个降级警告
- ✅ G-code仍在边界内
---
## 📊 性能影响评估
### 切片性能
| 阶段 | 性能影响 | 说明 |
|------|----------|------|
| 模型加载/处理 | < 0.1% | 无影响 |
| Skirt/Brim生成 | 1-2% | 验证开销 |
| 支撑生成 | 1-2% | 验证开销 |
| 擦料塔验证 | < 0.1% | 一次性检查 |
| G-code生成 | 0% | 无影响 |
| **总体** | **< 5%** | 用户无感知 |
### 内存影响
- 边界违规记录:每个约100字节
- 对于典型切片:< 1MB
- **影响**: 可忽略
---
## 🔙 回滚方案
### 快速回滚(如有问题)
**方法1: Git Revert**
```bash
# 回滚到修改前的commit
git revert <commit-hash>
git revert <commit-hash>
# ... 回滚所有相关commit
```
**方法2: 手动修改关键文件**
如果发现特定问题,可以临时禁用某些检查:
1. **禁用Travel检查** - `GCodeViewer.cpp:2403-2450`
```cpp
// 注释掉这段代码
/*
if (m_contained_in_bed) {
bool all_moves_valid = build_volume.all_moves_inside(...);
...
}
*/
```
2. **降低擦料塔检查严格度** - `Print.cpp:1290-1327`
```cpp
// 改为警告而非错误
BOOST_LOG_TRIVIAL(warning) << "Wipe tower outside bounds";
// 不要 return 错误
```
3. **禁用抬升降级** - `GCodeWriter.cpp:574-591, 633-649`
```cpp
// 注释掉降级逻辑,保持原有类型
```
---
## 📝 发布说明模板
### 更新日志建议
```
边界检测增强 (v2.x.0)
====================
✨ 新功能
- 添加Travel移动边界验证,防止打印头超出范围
- 添加擦料塔位置验证,避免设置错误
- 添加Skirt/Brim/支撑边界检查
- 添加螺旋/懒惰抬升自动降级机制
🔧 改进
- 提升边界检测精度和覆盖率
- 优化边缘区域的打印安全性
⚠️ 重要提示
- 如果擦料塔位置超出打印床范围,切片将失败
- 边缘区域的抬升方式可能自动调整(螺旋→懒惰→普通)
- 建议:将物体放置在距离边缘至少5mm的位置
🐛 修复
- 修复Travel移动可能超出边界的问题
- 修复擦料塔不验证位置的问题
```
---
## 📋 发布前检查清单
### 代码审查
- [x] 代码变更已审查
- [x] 遵循项目编码规范
- [x] 无内存泄漏风险
- [x] 边界条件已处理
### 测试验证
- [ ] TC01-TC06 测试用例全部通过
- [ ] 回归测试通过
- [ ] 性能测试通过(切片时间增加<5%)
- [ ] 边缘打印场景验证
### 文档
- [x] 实施文档完整
- [x] 风险评估完成
- [ ] 更新日志已准备
- [ ] 用户文档已更新(如需要)
### 兼容性
- [ ] Windows编译通过
- [ ] macOS编译通过(如支持)
- [ ] Linux编译通过(如支持)
- [ ] 现有配置文件兼容
---
## 🎯 风险总结
### 优点 ✅
1. **安全性大幅提升**
- 防止打印头撞机
- 防止超出边界的移动
- 自动降级保护
2. **用户体验改善**
- 更清晰的错误信息
- 自动处理临界情况
- 无需手动干预
3. **代码质量提升**
- 统一的验证框架
- 可扩展架构
- 详细日志记录
### 潜在问题 ⚠️
1. **擦料塔位置错误**
- **影响**: 切片失败
- **概率**: 中
- **缓解**: 清晰错误信息
2. **边缘打印行为改变**
- **影响**: 抬升方式可能不同
- **概率**: 低(仅边缘)
- **缓解**: 自动降级
3. **现有配置可能需要调整**
- **影响**: 可能显示新警告
- **概率**: 低
- **缓解**: 调整参数
---
## 📞 问题响应预案
### 如果用户报告问题
**问题1**: "之前能切片,现在失败了"
- **可能原因**: 擦料塔位置超出边界
- **解决方案**:
```
检查擦料塔位置设置(打印机设置 → 高级 → 擦料塔位置)
确保在打印床范围内(考虑brim宽度)
```
**问题2**: "显示toolpath_outside警告"
- **可能原因**: Travel移动超出边界
- **解决方案**:
```
检查物体是否过于靠近床边缘
检查擦料塔位置
尝试将物体向中心移动5-10mm
```
**问题3**: "打印质量下降"
- **可能原因**: 抬升方式改变(边缘)
- **解决方案**:
```
关闭"螺旋抬升"选项
或将物体远离边缘
```
---
## 📈 建议发布策略
### 渐进式发布(推荐)
1. **Beta测试** (1-2周)
- 发布给内部测试人员
- 收集反馈
- 修复发现的问题
2. **RC发布** (1周)
- 发布给早期采用者
- 监控反馈
- 准备回滚方案
3. **正式发布**
- 包含详细更新日志
- 提供迁移指南
- 监控用户反馈
### 回滚触发条件
如果出现以下情况,考虑回滚:
- ❌ 大量用户报告切片失败
- ❌ 发现严重打印质量问题
- ❌ 性能下降超过10%
---
## ✅ 最终建议
### 建议:**可以发布,但需要充分测试**
**理由**:
1. ✅ 核心功能实现正确
2. ✅ 有完善的降级机制
3. ✅ 风险可控且可识别
4. ✅ 有清晰的回滚方案
**前提条件**:
1. ⚠️ **必须**通过TC01-TC06测试
2. ⚠️ **必须**准备更新日志和用户指南
3. ⚠️ **建议**先进行Beta测试
**发布后监控**:
- 关注用户反馈
- 监控错误报告
- 准备快速补丁
---
**文档结束**
**风险评估**: 🟡 中等风险
**建议**: ✅ 建议发布(充分测试后)
**最后更新**: 2026-01-20
-66
View File
@@ -4331,72 +4331,6 @@ msgstr ""
msgid "A G-code path goes beyond the plate boundaries."
msgstr ""
msgid "G-code boundary violations detected:\n\n"
msgstr ""
msgid "Travel Move"
msgstr ""
msgid "Extrude Move"
msgstr ""
msgid "Spiral Lift"
msgstr ""
msgid "Lazy Lift"
msgstr ""
msgid "Wipe Tower"
msgstr ""
msgid "Skirt"
msgstr ""
msgid "Arc Move"
msgstr ""
msgid "violation(s)"
msgstr ""
msgid "violations"
msgstr ""
msgid "Total"
msgstr ""
msgid "Details"
msgstr ""
msgid "at Z"
msgstr ""
msgid "%.2f mm out"
msgstr ""
msgid "... and more"
msgstr ""
msgid "beyond X minimum"
msgstr ""
msgid "beyond X maximum"
msgstr ""
msgid "beyond Y minimum"
msgstr ""
msgid "beyond Y maximum"
msgstr ""
msgid "above Z maximum"
msgstr ""
msgid "beyond bed radius"
msgstr ""
msgid "outside boundaries"
msgstr ""
msgid "Only the object being edited is visible."
msgstr ""
@@ -4257,63 +4257,6 @@ msgstr "检测出超出打印高度的G-code路径。"
msgid "A G-code path goes beyond the plate boundaries."
msgstr "检测超出热床边界的G-code路径。"
msgid "G-code boundary violations detected:\n\n"
msgstr "检测到G-code超出边界:\n\n"
msgid "Travel Move"
msgstr "移动"
msgid "Extrude Move"
msgstr "挤出移动"
msgid "Spiral Lift"
msgstr "螺旋抬升"
msgid "Lazy Lift"
msgstr "惰性抬升"
msgid "Wipe Tower"
msgstr "擦料塔"
msgid "Arc Move"
msgstr "圆弧移动"
msgid "violation(s)"
msgstr "处违规"
msgid "violations"
msgstr "违规"
msgid "at Z"
msgstr "在Z"
msgid "%.2f mm out"
msgstr "超出%.2f毫米"
msgid "... and more"
msgstr "...以及更多"
msgid "beyond X minimum"
msgstr "超出X最小值"
msgid "beyond X maximum"
msgstr "超出X最大值"
msgid "beyond Y minimum"
msgstr "超出Y最小值"
msgid "beyond Y maximum"
msgstr "超出Y最大值"
msgid "above Z maximum"
msgstr "高于Z最大值"
msgid "beyond bed radius"
msgstr "超出热床半径"
msgid "outside boundaries"
msgstr "超出边界"
msgid "Only the object being edited is visible."
msgstr "只有正在编辑的对象是可见的。"
-215
View File
@@ -1,215 +0,0 @@
#include "BoundaryValidator.hpp"
#include "Geometry.hpp"
#include "libslic3r.h"
#include <cmath>
namespace Slic3r {
// ============================================================================
// BoundaryValidator static methods
// ============================================================================
std::string BoundaryValidator::violation_type_name(ViolationType type)
{
switch (type) {
case ViolationType::Unknown:
return "Unknown";
case ViolationType::TravelMove:
return "Travel Move";
case ViolationType::ExtrudeMove:
return "Extrude Move";
case ViolationType::SpiralLift:
return "Spiral Lift";
case ViolationType::LazyLift:
return "Lazy Lift";
case ViolationType::WipeTower:
return "Wipe Tower";
case ViolationType::Skirt:
return "Skirt";
case ViolationType::Brim:
return "Brim";
case ViolationType::Support:
return "Support";
case ViolationType::ArcMove:
return "Arc Move";
default:
return "Unknown Violation";
}
}
// ============================================================================
// BuildVolumeBoundaryValidator implementation
// ============================================================================
BuildVolumeBoundaryValidator::BuildVolumeBoundaryValidator(
const BuildVolume& build_volume,
double epsilon)
: m_build_volume(build_volume), m_epsilon(epsilon)
{
}
bool BuildVolumeBoundaryValidator::validate_point(const Vec3d& point) const
{
// Validate Z height first (if printable_height is set)
if (m_build_volume.printable_height() > 0.0) {
if (point.z() > m_build_volume.printable_height() + m_epsilon) {
return false;
}
}
// Validate XY position based on build volume type
return is_inside_2d(Vec2d(point.x(), point.y()));
}
bool BuildVolumeBoundaryValidator::validate_line(const Vec3d& from, const Vec3d& to) const
{
// For line validation, we sample multiple points along the line
// to ensure the entire segment is within boundaries
const int num_samples = 10;
for (int i = 0; i <= num_samples; ++i) {
double t = static_cast<double>(i) / num_samples;
Vec3d sample_point = from + t * (to - from);
if (!validate_point(sample_point)) {
return false;
}
}
return true;
}
bool BuildVolumeBoundaryValidator::validate_arc(
const Vec3d& center,
double radius,
double start_angle,
double end_angle,
double z_height) const
{
// Sample points along the arc and validate each
std::vector<Vec3d> arc_points = sample_arc_points(
center, radius, start_angle, end_angle, z_height
);
for (const Vec3d& point : arc_points) {
if (!validate_point(point)) {
return false;
}
}
return true;
}
bool BuildVolumeBoundaryValidator::validate_polygon(const Polygon& poly, double z_height) const
{
// Check if Z height is valid
if (m_build_volume.printable_height() > 0.0) {
if (z_height > m_build_volume.printable_height() + m_epsilon) {
return false;
}
}
// Check all polygon vertices
for (const Point& pt : poly.points) {
Vec2d unscaled_pt = unscale(pt);
if (!is_inside_2d(unscaled_pt)) {
return false;
}
}
return true;
}
std::vector<Vec3d> BuildVolumeBoundaryValidator::sample_arc_points(
const Vec3d& center,
double radius,
double start_angle,
double end_angle,
double z_height,
int num_samples) const
{
std::vector<Vec3d> points;
points.reserve(num_samples);
// Handle angle wrapping (e.g., from 350° to 10° should go through 360°/0°)
double angle_range = end_angle - start_angle;
// Normalize to handle wrapping
if (angle_range < 0) {
angle_range += 2 * PI;
}
for (int i = 0; i < num_samples; ++i) {
double t = static_cast<double>(i) / (num_samples - 1);
double angle = start_angle + t * angle_range;
double x = center.x() + radius * std::cos(angle);
double y = center.y() + radius * std::sin(angle);
points.emplace_back(x, y, z_height);
}
return points;
}
bool BuildVolumeBoundaryValidator::is_inside_2d(const Vec2d& point) const
{
const BuildVolume_Type type = m_build_volume.type();
switch (type) {
case BuildVolume_Type::Rectangle:
{
// Get the bounding box of the build volume
const BoundingBoxf& bbox = m_build_volume.bounding_volume2d();
BoundingBoxf inflated_bbox = bbox;
inflated_bbox.min -= Vec2d(m_epsilon, m_epsilon);
inflated_bbox.max += Vec2d(m_epsilon, m_epsilon);
return inflated_bbox.contains(point);
}
case BuildVolume_Type::Circle:
{
// Get circle parameters - circle.center is already in scaled coordinates
const Geometry::Circled& circle = m_build_volume.circle();
const Vec2d center_unscaled(unscale<double>(circle.center.x()),
unscale<double>(circle.center.y()));
const double radius = unscale<double>(circle.radius) + m_epsilon;
// Check distance from center
double dist_sq = (point - center_unscaled).squaredNorm();
return dist_sq <= radius * radius;
}
case BuildVolume_Type::Convex:
case BuildVolume_Type::Custom:
{
// For convex/custom volumes, use point-in-polygon test
// Get the convex hull decomposition - this returns pair<top, bottom>
const auto& decomp = m_build_volume.top_bottom_convex_hull_decomposition_bed();
const std::vector<Vec2d>& top_hull = decomp.first;
if (top_hull.empty()) {
return false;
}
// Check if point is inside the top convex hull
Point scaled_point = scaled<coord_t>(point);
// Build polygon from Vec2d points
Polygon hull_poly;
for (const Vec2d& pt : top_hull) {
hull_poly.points.push_back(scaled<coord_t>(pt));
}
return hull_poly.contains(scaled_point);
}
case BuildVolume_Type::Invalid:
default:
// If build volume type is invalid, allow everything (fail-safe)
return true;
}
}
} // namespace Slic3r
-168
View File
@@ -1,168 +0,0 @@
#ifndef slic3r_BoundaryValidator_hpp_
#define slic3r_BoundaryValidator_hpp_
#include "Point.hpp"
#include "Polygon.hpp"
#include "BuildVolume.hpp"
#include <vector>
#include <string>
namespace Slic3r {
/**
* @brief Abstract interface for validating geometric elements against print boundaries
*
* This class provides a unified interface for boundary validation across different
* parts of the slicing pipeline. Implementations can validate points, lines, arcs,
* and polygons against the build volume.
*/
class BoundaryValidator {
public:
/**
* @brief Types of boundary violations that can occur
*/
enum class ViolationType {
Unknown = 0,
TravelMove, // Travel move exceeds boundaries
ExtrudeMove, // Extrude move exceeds boundaries
SpiralLift, // Spiral lift arc exceeds boundaries
LazyLift, // Lazy lift slope exceeds boundaries
WipeTower, // Wipe tower position exceeds boundaries
Skirt, // Skirt exceeds boundaries
Brim, // Brim exceeds boundaries
Support, // Support material exceeds boundaries
ArcMove, // G2/G3 arc move exceeds boundaries
Count // Sentinel value
};
/**
* @brief Direction of boundary violation
*/
enum class BoundaryDirection {
Unknown = 0,
X_Min, // Beyond X minimum boundary
X_Max, // Beyond X maximum boundary
Y_Min, // Beyond Y minimum boundary
Y_Max, // Beyond Y maximum boundary
Z_Max, // Above Z maximum boundary
Radius, // Beyond circular bed radius
Count // Sentinel value
};
/**
* @brief Describes a single boundary violation
*/
struct BoundaryViolation {
ViolationType type; // Type of violation
BoundaryDirection direction; // Direction of violation
std::string description; // Human-readable description
Vec3d position; // Position where violation occurs (unscaled)
double distance_out; // How far outside boundaries (unscaled)
double layer_z; // Z height of the layer (unscaled)
std::string object_name; // Name of related object (if applicable)
BoundaryViolation(ViolationType t, const std::string& desc,
const Vec3d& pos, double z, const std::string& obj = "",
BoundaryDirection dir = BoundaryDirection::Unknown, double dist = 0.0)
: type(t), direction(dir), description(desc), position(pos), distance_out(dist), layer_z(z), object_name(obj) {}
};
using BoundaryViolations = std::vector<BoundaryViolation>;
virtual ~BoundaryValidator() = default;
/**
* @brief Validate a single point against boundaries
* @param point Point to validate (unscaled coordinates)
* @return true if point is within boundaries, false otherwise
*/
virtual bool validate_point(const Vec3d& point) const = 0;
/**
* @brief Validate a line segment against boundaries
* @param from Start point (unscaled coordinates)
* @param to End point (unscaled coordinates)
* @return true if entire line is within boundaries, false otherwise
*/
virtual bool validate_line(const Vec3d& from, const Vec3d& to) const = 0;
/**
* @brief Validate an arc path against boundaries
* @param center Arc center point (unscaled coordinates)
* @param radius Arc radius (unscaled)
* @param start_angle Start angle in radians
* @param end_angle End angle in radians
* @param z_height Z height of the arc (unscaled)
* @return true if entire arc is within boundaries, false otherwise
*/
virtual bool validate_arc(const Vec3d& center, double radius,
double start_angle, double end_angle,
double z_height) const = 0;
/**
* @brief Validate a polygon against boundaries
* @param poly Polygon to validate (scaled coordinates)
* @param z_height Z height of the polygon (unscaled)
* @return true if entire polygon is within boundaries, false otherwise
*/
virtual bool validate_polygon(const Polygon& poly, double z_height = 0.0) const = 0;
/**
* @brief Get human-readable name for violation type
*/
static std::string violation_type_name(ViolationType type);
};
/**
* @brief Concrete implementation of BoundaryValidator based on BuildVolume
*
* This validator uses the BuildVolume class to perform boundary checks.
* It supports all build volume types (Rectangle, Circle, Convex, Custom).
*/
class BuildVolumeBoundaryValidator : public BoundaryValidator {
public:
/**
* @brief Construct validator from BuildVolume
* @param build_volume Reference to the build volume
* @param epsilon Tolerance for boundary checks (default: BedEpsilon)
*/
explicit BuildVolumeBoundaryValidator(const BuildVolume& build_volume,
double epsilon = BuildVolume::BedEpsilon);
bool validate_point(const Vec3d& point) const override;
bool validate_line(const Vec3d& from, const Vec3d& to) const override;
bool validate_arc(const Vec3d& center, double radius,
double start_angle, double end_angle,
double z_height) const override;
bool validate_polygon(const Polygon& poly, double z_height = 0.0) const override;
private:
const BuildVolume& m_build_volume;
double m_epsilon;
/**
* @brief Sample points along an arc for validation
* @param center Arc center (unscaled)
* @param radius Arc radius (unscaled)
* @param start_angle Start angle in radians
* @param end_angle End angle in radians
* @param z_height Z height (unscaled)
* @param num_samples Number of sample points (default: 16)
* @return Vector of sampled points
*/
std::vector<Vec3d> sample_arc_points(const Vec3d& center, double radius,
double start_angle, double end_angle,
double z_height,
int num_samples = 16) const;
/**
* @brief Check if a 2D point is inside the build volume
* @param point 2D point (unscaled)
* @return true if inside, false otherwise
*/
bool is_inside_2d(const Vec2d& point) const;
};
} // namespace Slic3r
#endif // slic3r_BoundaryValidator_hpp_
+2 -61
View File
@@ -8,9 +8,6 @@
#include "libslic3r.h"
#include "PrintConfig.hpp"
#include "Model.hpp"
#include "BoundaryValidator.hpp"
#include "BuildVolume.hpp"
#include "GCode/GCodeProcessor.hpp"
#include <algorithm>
#include <numeric>
#include <unordered_set>
@@ -990,7 +987,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
polygons_reverse(ex_poly_holes_reversed);
if (has_outer_brim) {
// Snapmaker: inner and outer boundary are offset from the same polygon incase of round off error.
// BBS: inner and outer boundary are offset from the same polygon incase of round off error.
auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION);
ExPolygons outerExpoly;
if (use_brim_ears) {
@@ -1693,8 +1690,7 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::vector<std::pair<ObjectID, unsigned int>> &objPrintVec,
std::vector<unsigned int>& printExtruders,
Print* print_ptr)
std::vector<unsigned int>& printExtruders)
{
double brim_width_max = 0;
@@ -1742,68 +1738,13 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
for (size_t iia = 0; iia < islands_area.size(); ++iia)
islands_area[iia].translate(plate_shift);
// Snapmaker: Create BuildVolume and BoundaryValidator for brim boundary checking
BuildVolume build_volume(print.config().printable_area.values, print.config().printable_height);
BuildVolumeBoundaryValidator validator(build_volume);
double first_layer_height = print.skirt_first_layer_height();
for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) {
if (!iter->second.empty()) {
// Snapmaker: Validate brim area against build volume boundaries
for (const ExPolygon& expoly : iter->second) {
if (!validator.validate_polygon(expoly.contour, first_layer_height)) {
// Record boundary violation
if (print_ptr) {
BoundingBox bbox = get_extents(expoly.contour);
Vec3d violation_pos(
unscale<double>(bbox.center().x()),
unscale<double>(bbox.center().y()),
first_layer_height
);
PrintObject* obj = const_cast<PrintObject*>(print.get_object(iter->first));
std::string obj_name = obj ? obj->model_object()->name : "Unknown";
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Brim),
violation_pos,
first_layer_height,
obj_name
);
print_ptr->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Brim for object " << obj_name
<< " exceeds build volume boundaries at z=" << first_layer_height << " mm";
}
}
}
brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area)));
};
}
for (auto iter = supportBrimAreaMap.begin(); iter != supportBrimAreaMap.end(); ++iter) {
if (!iter->second.empty()) {
// Snapmaker: Validate support brim area against build volume boundaries
for (const ExPolygon& expoly : iter->second) {
if (!validator.validate_polygon(expoly.contour, first_layer_height)) {
// Record boundary violation
if (print_ptr) {
BoundingBox bbox = get_extents(expoly.contour);
Vec3d violation_pos(
unscale<double>(bbox.center().x()),
unscale<double>(bbox.center().y()),
first_layer_height
);
PrintObject* obj = const_cast<PrintObject*>(print.get_object(iter->first));
std::string obj_name = obj ? obj->model_object()->name : "Unknown";
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Brim),
violation_pos,
first_layer_height,
obj_name + " (support brim)"
);
print_ptr->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Support brim for object " << obj_name
<< " exceeds build volume boundaries at z=" << first_layer_height << " mm";
}
}
}
supportBrimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area)));
};
}
+1 -3
View File
@@ -15,13 +15,11 @@ class ObjectID;
// Produce brim lines around those objects, that have the brim enabled.
// Collect islands_area to be merged into the final 1st layer convex hull.
// If print_ptr is provided (non-const), boundary violations will be reported.
void make_brim(const Print& print, PrintTryCancel try_cancel,
Polygons& islands_area, std::map<ObjectID, ExtrusionEntityCollection>& brimMap,
std::map<ObjectID, ExtrusionEntityCollection>& supportBrimMap,
std::vector<std::pair<ObjectID, unsigned int>>& objPrintVec,
std::vector<unsigned int>& printExtruders,
Print* print_ptr = nullptr);
std::vector<unsigned int>& printExtruders);
// BBS: automatically make brim
ExtrusionEntityCollection make_brim_auto(const Print &print, PrintTryCancel try_cancel, Polygons &islands_area);
-2
View File
@@ -75,8 +75,6 @@ set(lisbslic3r_sources
BlacklistedLibraryCheck.hpp
BoundingBox.cpp
BoundingBox.hpp
BoundaryValidator.cpp
BoundaryValidator.hpp
BridgeDetector.cpp
BridgeDetector.hpp
Brim.cpp
-7
View File
@@ -21,8 +21,6 @@
#include "libslic3r/format.hpp"
#include "Time.hpp"
#include "GCode/ExtrusionProcessor.hpp"
#include "BoundaryValidator.hpp"
#include "BuildVolume.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
@@ -1892,11 +1890,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato
m_writer.set_is_bbl_machine(is_bbl_printers);
// Snapmaker: Initialize boundary validator for arc path validation
BuildVolume build_volume(print.config().printable_area.values, print.config().printable_height);
BuildVolumeBoundaryValidator validator(build_volume);
m_writer.set_boundary_validator(&validator, &print);
// How many times will be change_layer() called?
// change_layer() in turn increments the progress bar status.
m_layer_count = 0;
-2
View File
@@ -574,7 +574,6 @@ void GCodeProcessorResult::reset() {
custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
spiral_vase_layers = std::vector<std::pair<float, std::pair<size_t, size_t>>>();
time = 0;
boundary_violations.clear();
//BBS: add mutex for protection of gcode result
unlock();
@@ -608,7 +607,6 @@ void GCodeProcessorResult::reset() {
spiral_vase_layers = std::vector<std::pair<float, std::pair<size_t, size_t>>>();
bed_match_result = BedMatchResult(true);
warnings.clear();
boundary_violations.clear();
//BBS: add mutex for protection of gcode result
unlock();
+1 -65
View File
@@ -6,7 +6,6 @@
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/BoundaryValidator.hpp"
#include <cstdint>
#include <array>
@@ -105,66 +104,18 @@ class Print;
}
};
// Forward declaration for BoundaryValidator
class BoundaryValidator;
struct ConflictResult
{
// ===== Existing fields for object collision =====
std::string _objName1;
std::string _objName2;
double _height;
const void *_obj1; // nullptr means wipe tower
const void *_obj2;
int layer = -1;
// ===== New fields for boundary violations =====
enum class ConflictType {
ObjectCollision, // Original: collision between objects
BoundaryViolation // New: path exceeds build volume boundaries
};
ConflictType conflict_type = ConflictType::ObjectCollision;
// Only valid when conflict_type == BoundaryViolation
int violation_type_int = -1; // Stores BoundaryValidator::ViolationType as int
Vec3d violation_position{Vec3d::Zero()}; // Position where violation occurs (unscaled)
// ===== Constructors =====
ConflictResult(const std::string &objName1, const std::string &objName2, double height, const void *obj1, const void *obj2)
: _objName1(objName1), _objName2(objName2), _height(height), _obj1(obj1), _obj2(obj2),
conflict_type(ConflictType::ObjectCollision)
: _objName1(objName1), _objName2(objName2), _height(height), _obj1(obj1), _obj2(obj2)
{}
ConflictResult() = default;
// New: Static factory method for boundary violations
// Note: violation_type should be cast from BoundaryValidator::ViolationType
static ConflictResult create_boundary_violation(
int violation_type,
const Vec3d& pos,
double height,
const std::string& obj_name = ""
) {
ConflictResult result;
result.conflict_type = ConflictType::BoundaryViolation;
result.violation_type_int = violation_type;
result.violation_position = pos;
result._height = height;
result._objName1 = obj_name;
result.layer = -1; // Will be computed later if needed
return result;
}
// Helper method to check if this is a boundary violation
bool is_boundary_violation() const {
return conflict_type == ConflictType::BoundaryViolation;
}
// Helper method to check if this is an object collision
bool is_object_collision() const {
return conflict_type == ConflictType::ObjectCollision;
}
};
struct BedMatchResult
@@ -240,18 +191,6 @@ class Print;
std::vector<std::string> params; // extra msg info
};
// Snapmaker: Detailed boundary violation information for better user feedback
// Uses BoundaryValidator enums to avoid duplication
struct BoundaryViolationInfo {
BoundaryValidator::ViolationType violation_type{BoundaryValidator::ViolationType::Unknown};
BoundaryValidator::BoundaryDirection direction{BoundaryValidator::BoundaryDirection::Unknown};
Vec3d position{Vec3d::Zero()}; // Position where violation occurs (mm)
double distance_out{0.0}; // How far outside (mm)
std::string component_name; // e.g., "Skirt", "Brim", "Support", "Wipe Tower"
int layer_num{-1}; // Layer number (if applicable)
float print_z{-1.0f}; // Z height at violation (mm)
};
std::string filename;
unsigned int id;
std::vector<MoveVertex> moves;
@@ -283,8 +222,6 @@ class Print;
std::vector<std::pair<float, std::pair<size_t, size_t>>> spiral_vase_layers;
//BBS
std::vector<SliceWarning> warnings;
// Snapmaker: Detailed boundary violation information
std::vector<BoundaryViolationInfo> boundary_violations;
int nozzle_hrc;
NozzleType nozzle_type;
BedType bed_type = BedType::btCount;
@@ -318,7 +255,6 @@ class Print;
custom_gcode_per_print_z = other.custom_gcode_per_print_z;
spiral_vase_layers = other.spiral_vase_layers;
warnings = other.warnings;
boundary_violations = other.boundary_violations;
bed_type = other.bed_type;
bed_match_result = other.bed_match_result;
#if ENABLE_GCODE_VIEWER_STATISTICS
+14 -158
View File
@@ -1,15 +1,11 @@
#include "GCodeWriter.hpp"
#include "CustomGCode.hpp"
#include "BoundaryValidator.hpp"
#include "BuildVolume.hpp"
#include "Print.hpp"
#include "GCode/GCodeProcessor.hpp"
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <assert.h>
#include <cmath>
#include <GCode/GCodeProcessor.hpp>
#ifdef __APPLE__
#include <boost/spirit/include/karma.hpp>
@@ -549,116 +545,29 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
if (delta(2) > 0 && delta_no_z.norm() != 0.0f) {
//BBS: SpiralLift
if (m_to_lift_type == LiftType::SpiralLift && this->is_current_position_clear()) {
// Calculate the radius of the spiral arc
//BBS: todo: check the arc move all in bed area, if not, then use lazy lift
double radius = delta(2) / (2 * PI * atan(this->extruder()->travel_slope()));
// Calculate arc center and angles for precise boundary validation
Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) };
// Arc center is source + ij_offset (in unscaled coordinates)
Vec3d arc_center = source + Vec3d(ij_offset(0), ij_offset(1), 0);
// Calculate start and end angles
// ij_offset is perpendicular to delta_no_z, so the arc starts from -ij_offset direction
double start_angle = std::atan2(-ij_offset(1), -ij_offset(0));
double end_angle = start_angle + 2 * PI; // Full circle
// Snapmaker: Use BoundaryValidator for precise arc validation
bool arc_valid = true;
if (m_boundary_validator) {
arc_valid = m_boundary_validator->validate_arc(
arc_center, radius, start_angle, end_angle, source.z()
);
if (!arc_valid) {
// Record boundary violation
if (m_print_ptr) {
Vec3d violation_pos = arc_center + Vec3d(radius, 0, source.z());
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::SpiralLift),
violation_pos,
source.z(),
"Spiral Lift"
);
m_print_ptr->add_boundary_violation(violation);
}
BOOST_LOG_TRIVIAL(warning) << "Spiral lift arc exceeds build volume boundaries, "
<< "downgrading to lazy lift. Center: (" << arc_center.x() << ", " << arc_center.y()
<< "), Radius: " << radius << " mm";
// Fall through to LazyLift check below
m_to_lift_type = LiftType::LazyLift;
}
} else {
// Fallback: Simple radius check if validator not available
constexpr double MAX_SAFE_SPIRAL_RADIUS = 50.0; // mm
if (radius > MAX_SAFE_SPIRAL_RADIUS) {
BOOST_LOG_TRIVIAL(warning) << "Spiral lift radius (" << radius
<< " mm) exceeds safe limit (" << MAX_SAFE_SPIRAL_RADIUS
<< " mm), downgrading to lazy lift to prevent boundary violations";
m_to_lift_type = LiftType::LazyLift;
arc_valid = false;
}
}
if (arc_valid) {
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
}
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
}
//BBS: LazyLift
if (m_to_lift_type == LiftType::LazyLift &&
else if (m_to_lift_type == LiftType::LazyLift &&
this->is_current_position_clear() &&
atan2(delta(2), delta_no_z.norm()) < this->extruder()->travel_slope()) {
// Calculate the slope top point
//BBS: check whether we can make a travel like
// _____
// / to make the z list early to avoid to hit some warping place when travel is long.
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->extruder()->travel_slope());
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
// Snapmaker: Use BoundaryValidator for precise line validation
bool slope_valid = true;
if (m_boundary_validator) {
// Validate the entire slope line from source to slope_top_point
slope_valid = m_boundary_validator->validate_line(source, slope_top_point);
if (!slope_valid) {
// Record boundary violation
if (m_print_ptr) {
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::LazyLift),
slope_top_point,
source.z(),
"Lazy Lift"
);
m_print_ptr->add_boundary_violation(violation);
}
BOOST_LOG_TRIVIAL(warning) << "Lazy lift slope exceeds build volume boundaries, "
<< "downgrading to normal lift. Slope point: (" << slope_top_point.x()
<< ", " << slope_top_point.y() << ", " << slope_top_point.z() << ")";
// Fall through to NormalLift
m_to_lift_type = LiftType::NormalLift;
}
} else {
// Fallback: Simple distance check if validator not available
constexpr double MAX_SAFE_SLOPE_DISTANCE = 100.0; // mm
double slope_distance = temp.norm();
if (slope_distance > MAX_SAFE_SLOPE_DISTANCE) {
BOOST_LOG_TRIVIAL(warning) << "Lazy lift slope distance (" << slope_distance
<< " mm) exceeds safe limit (" << MAX_SAFE_SLOPE_DISTANCE
<< " mm), downgrading to normal lift to prevent boundary violations";
m_to_lift_type = LiftType::NormalLift;
slope_valid = false;
}
}
if (slope_valid) {
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
w0.emit_f(travel_speed * 60.0);
//BBS
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
w0.emit_f(travel_speed * 60.0);
//BBS
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
if (m_to_lift_type == LiftType::NormalLift) {
else if (m_to_lift_type == LiftType::NormalLift) {
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
@@ -825,59 +734,6 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std:
//center_offset is I and J axis
std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion)
{
// Snapmaker: Validate arc path against build volume boundaries
if (m_boundary_validator) {
// Calculate arc center (center_offset is relative to start point)
Vec2d start_point = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset };
Vec3d arc_center = Vec3d(start_point(0) + center_offset(0), start_point(1) + center_offset(1), m_pos(2));
// Calculate radius from center offset
double radius = std::sqrt(center_offset(0) * center_offset(0) + center_offset(1) * center_offset(1));
// Calculate start and end angles
Vec2d start_vec = start_point - Vec2d(arc_center.x(), arc_center.y());
Vec2d end_vec = Vec2d(point(0) - m_x_offset, point(1) - m_y_offset) - Vec2d(arc_center.x(), arc_center.y());
double start_angle = std::atan2(start_vec(1), start_vec(0));
double end_angle = std::atan2(end_vec(1), end_vec(0));
// Handle CCW vs CW and angle wrapping
if (is_ccw) {
// For CCW, ensure end_angle > start_angle (wrapping if needed)
if (end_angle < start_angle) {
end_angle += 2 * PI;
}
} else {
// For CW, ensure end_angle < start_angle (wrapping if needed)
if (end_angle > start_angle) {
end_angle -= 2 * PI;
}
}
// Validate the arc
bool arc_valid = m_boundary_validator->validate_arc(
arc_center, radius, start_angle, end_angle, m_pos(2)
);
if (!arc_valid) {
// Record boundary violation
if (m_print_ptr) {
Vec3d violation_pos = arc_center + Vec3d(radius, 0, m_pos(2));
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::ArcMove),
violation_pos,
m_pos(2),
"Arc Extrusion"
);
m_print_ptr->add_boundary_violation(violation);
}
BOOST_LOG_TRIVIAL(warning) << "Arc extrusion path exceeds build volume boundaries. "
<< "Center: (" << arc_center.x() << ", " << arc_center.y()
<< "), Radius: " << radius << " mm, Z: " << m_pos(2) << " mm";
// Continue anyway (don't fail, just warn)
}
}
m_pos(0) = point(0);
m_pos(1) = point(1);
if (!force_no_extrusion)
-14
View File
@@ -11,10 +11,6 @@
namespace Slic3r {
// Forward declarations
class BoundaryValidator;
class Print;
class GCodeWriter {
public:
GCodeConfig config;
@@ -123,12 +119,6 @@ public:
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
// Snapmaker: Set boundary validator for arc path validation
void set_boundary_validator(const BoundaryValidator* validator, Print* print_ptr = nullptr) {
m_boundary_validator = validator;
m_print_ptr = print_ptr;
}
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
private:
@@ -180,10 +170,6 @@ public:
double m_current_speed;
bool m_is_first_layer = true;
// Snapmaker: Boundary validator for arc path validation
const BoundaryValidator* m_boundary_validator = nullptr;
Print* m_print_ptr = nullptr;
enum class Acceleration {
Travel,
Print
+1 -119
View File
@@ -36,7 +36,6 @@
#include "nlohmann/json.hpp"
#include "GCode/ConflictChecker.hpp"
#include "BoundaryValidator.hpp"
#include <codecvt>
@@ -1288,45 +1287,6 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
}
}
}
// Snapmaker: Critical fix - Validate wipe tower position is within build volume boundaries
// This addresses vulnerability #3: Wipe tower position was not validated against bed boundaries
{
const size_t plate_index = this->get_plate_index();
const Vec3d plate_origin = this->get_plate_origin();
const float x = m_config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
const float y = m_config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
const float width = m_config.prime_tower_width.value;
const float brim_width = m_config.prime_tower_brim_width.value;
const float depth = this->wipe_tower_data(extruders.size()).depth;
// Check all four corners of wipe tower (including brim)
// Create a simple bounding box from printable_area config
BoundingBoxf bed_bbox;
for (const Vec2d& pt : m_config.printable_area.values) {
bed_bbox.merge(pt);
}
bool tower_outside = false;
// Check all corners
if (x - brim_width < bed_bbox.min.x() || x + width + brim_width > bed_bbox.max.x() ||
y - brim_width < bed_bbox.min.y() || y + depth + brim_width > bed_bbox.max.y()) {
tower_outside = true;
}
if (tower_outside) {
const float total_width = width + 2 * brim_width;
const float total_depth = depth + 2 * brim_width;
return StringObjectException{
Slic3r::format(_u8L("The prime tower at position (%.2f, %.2f) with dimensions %.2f x %.2f mm "
"(including %.2f mm brim) exceeds the bed boundaries. "
"Please adjust the prime tower position in the configuration."),
x, y, total_width, total_depth, brim_width),
nullptr,
"wipe_tower_x"
};
}
}
}
{
@@ -2188,7 +2148,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_brim()) {
Polygons islands_area;
make_brim(*this, this->make_try_cancel(), islands_area, m_brimMap,
m_supportBrimMap, objPrintVec, printExtruders, this);
m_supportBrimMap, objPrintVec, printExtruders);
for (Polygon& poly_ex : islands_area)
poly_ex.douglas_peucker(SCALED_RESOLUTION);
for (Polygon &poly : union_(this->first_layer_islands(), islands_area))
@@ -2287,37 +2247,6 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
//BBS
result->conflict_result = m_conflict_result;
// Snapmaker: Copy boundary violations from Print to GCodeProcessorResult
// This allows detailed violation information to be displayed in the GUI
if (!m_boundary_violations.empty()) {
result->boundary_violations.clear();
result->boundary_violations.reserve(m_boundary_violations.size());
for (const auto& conflict : m_boundary_violations) {
if (conflict.is_boundary_violation()) {
GCodeProcessorResult::BoundaryViolationInfo info;
// Directly copy the violation type (enums are now unified)
info.violation_type = static_cast<BoundaryValidator::ViolationType>(conflict.violation_type_int);
// Copy position and height
info.position = conflict.violation_position;
info.print_z = static_cast<float>(conflict._height);
info.layer_num = conflict.layer;
// Direction is not directly available in ConflictResult, leave as Unknown
info.direction = BoundaryValidator::BoundaryDirection::Unknown;
info.distance_out = 0.0;
result->boundary_violations.push_back(info);
}
}
BOOST_LOG_TRIVIAL(info) << "Copied " << result->boundary_violations.size()
<< " boundary violations from Print to GCodeProcessorResult";
}
return path.c_str();
}
@@ -2414,11 +2343,6 @@ void Print::_make_skirt()
// Draw outlines from outside to inside.
// Loop while we have less skirts than required or any extruder hasn't reached the min length if any.
std::vector<coordf_t> extruded_length(extruders.size(), 0.);
// Create BuildVolume and BoundaryValidator for skirt boundary checking
BuildVolume build_volume(m_config.printable_area.values, m_config.printable_height);
BuildVolumeBoundaryValidator validator(build_volume);
if (m_config.skirt_type == stCombined) {
for (size_t i = m_config.skirt_loops, extruder_idx = 0; i > 0; -- i) {
this->throw_if_canceled();
@@ -2434,28 +2358,6 @@ void Print::_make_skirt()
break;
loop = loops.front();
}
// Snapmaker: Validate skirt loop against build volume boundaries
if (!validator.validate_polygon(loop, initial_layer_print_height)) {
// Record boundary violation
BoundingBox loop_bbox = get_extents(loop);
Vec3d violation_pos(
unscale<double>(loop_bbox.center().x()),
unscale<double>(loop_bbox.center().y()),
initial_layer_print_height
);
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Skirt),
violation_pos,
initial_layer_print_height,
"Skirt"
);
this->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Skirt loop exceeds build volume boundaries at z="
<< initial_layer_print_height << " mm";
// Continue with remaining loops but record the violation
}
// Extrude the skirt loop.
ExtrusionLoop eloop(elrSkirt);
eloop.paths.emplace_back(ExtrusionPath(
@@ -2514,26 +2416,6 @@ void Print::_make_skirt()
loop = loops.front();
}
// Snapmaker: Validate per-object skirt loop against build volume boundaries
if (!validator.validate_polygon(loop, initial_layer_print_height)) {
// Record boundary violation
BoundingBox loop_bbox = get_extents(loop);
Vec3d violation_pos(
unscale<double>(loop_bbox.center().x()),
unscale<double>(loop_bbox.center().y()),
initial_layer_print_height
);
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Skirt),
violation_pos,
initial_layer_print_height,
object->model_object()->name
);
this->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Per-object skirt loop for " << object->model_object()->name
<< " exceeds build volume boundaries at z=" << initial_layer_print_height << " mm";
}
// Extrude the skirt loop.
ExtrusionLoop eloop(elrSkirt);
eloop.paths.emplace_back(ExtrusionPath(
-19
View File
@@ -970,23 +970,6 @@ public:
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
// Snapmaker: boundary violations tracking
void add_boundary_violation(const ConflictResult& violation) {
m_boundary_violations.push_back(violation);
}
const std::vector<ConflictResult>& get_boundary_violations() const {
return m_boundary_violations;
}
void clear_boundary_violations() {
m_boundary_violations.clear();
}
bool has_boundary_violations() const {
return !m_boundary_violations.empty();
}
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
Points first_layer_wipe_tower_corners(bool check_wipe_tower_existance=true) const;
@@ -1078,8 +1061,6 @@ private:
int m_modified_count {0};
//BBS
ConflictResultOpt m_conflict_result;
//Snapmaker: boundary violations tracking
std::vector<ConflictResult> m_boundary_violations;
FakeWipeTower m_fake_wipe_tower;
//SoftFever: calibration
-54
View File
@@ -2,9 +2,6 @@
#include "Print.hpp"
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
#include "BoundaryValidator.hpp"
#include "BuildVolume.hpp"
#include "GCode/GCodeProcessor.hpp"
#include "ElephantFootCompensation.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
@@ -673,57 +670,6 @@ void PrintObject::generate_support_material()
this->_generate_support_material();
m_print->throw_if_canceled();
// Snapmaker: Validate support material against build volume boundaries
if (!m_support_layers.empty()) {
BuildVolume build_volume(m_print->config().printable_area.values, m_print->config().printable_height);
BuildVolumeBoundaryValidator validator(build_volume);
for (const SupportLayer* layer : m_support_layers) {
// Check support fills polygons
for (const Polygon& support_contour : layer->support_fills.polygons_covered_by_spacing()) {
// Apply instance transforms and check boundary
for (const PrintInstance& instance : this->instances()) {
Polygon translated_contour = support_contour;
translated_contour.translate(instance.shift);
// Convert to unscaled coordinates for validation
Vec3d plate_origin = m_print->get_plate_origin();
double z_height = layer->print_z;
// Check if any point of the contour exceeds boundaries
bool has_violation = false;
Vec3d violation_pos;
for (const Point& pt : translated_contour.points) {
Vec3d pt_unscaled(
unscale<double>(pt.x()) + plate_origin.x(),
unscale<double>(pt.y()) + plate_origin.y(),
z_height
);
if (!validator.validate_point(pt_unscaled)) {
has_violation = true;
violation_pos = pt_unscaled;
break;
}
}
if (has_violation) {
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Support),
violation_pos,
z_height,
this->model_object()->name
);
m_print->add_boundary_violation(violation);
BOOST_LOG_TRIVIAL(warning) << "Support material for object " << this->model_object()->name
<< " exceeds build volume boundaries at z=" << z_height << " mm";
// Only report once per layer to avoid spam
break;
}
}
}
}
}
}
this->set_done(posSupportMaterial);
}
-88
View File
@@ -8,9 +8,6 @@
#include "Geometry.hpp"
#include "Point.hpp"
#include "MutablePolygon.hpp"
#include "BoundaryValidator.hpp"
#include "BuildVolume.hpp"
#include "GCode/GCodeProcessor.hpp"
#include <cmath>
#include <memory>
@@ -584,91 +581,6 @@ void PrintObjectSupportMaterial::generate(PrintObject &object)
}
#endif /* SLIC3R_DEBUG */
// Snapmaker: Validate support polygons against build volume boundaries
// This addresses vulnerability #6: Support material boundary validation
BOOST_LOG_TRIVIAL(info) << "Support generator - Validating boundaries";
BuildVolume build_volume(object.print()->config().printable_area.values,
object.print()->config().printable_height);
BuildVolumeBoundaryValidator validator(build_volume);
int support_violations = 0;
for (const SupportLayer* layer : object.support_layers()) {
if (!layer)
continue;
// Check support extrusions
const ExtrusionEntityCollection& support_fills = layer->support_fills;
for (const ExtrusionEntity* entity : support_fills.entities) {
if (!entity)
continue;
// Check each extrusion path or loop
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(entity)) {
// Check each point in the polyline
for (const Point& pt : path->polyline.points) {
Vec3d pos(unscaled<double>(pt.x()), unscaled<double>(pt.y()), layer->print_z);
if (!validator.validate_point(pos)) {
support_violations++;
if (support_violations <= 5) { // Log first 5
BOOST_LOG_TRIVIAL(warning) << "Support path at z=" << layer->print_z
<< " exceeds build volume boundaries";
}
break; // Only record once per path
}
}
} else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(entity)) {
for (const ExtrusionPath& path : loop->paths) {
// Check each point in the polyline
for (const Point& pt : path.polyline.points) {
Vec3d pos(unscaled<double>(pt.x()), unscaled<double>(pt.y()), layer->print_z);
if (!validator.validate_point(pos)) {
support_violations++;
if (support_violations <= 5) { // Log first 5
BOOST_LOG_TRIVIAL(warning) << "Support loop path at z=" << layer->print_z
<< " exceeds build volume boundaries";
}
break; // Only record once per path
}
}
}
}
}
// Check support base polygons
for (const ExPolygon& expoly : layer->lslices) {
if (!validator.validate_polygon(expoly.contour, layer->print_z)) {
support_violations++;
if (support_violations <= 5) { // Log first 5
BOOST_LOG_TRIVIAL(warning) << "Support polygon at z=" << layer->print_z
<< " exceeds build volume boundaries";
}
}
// Check holes
for (const Polygon& hole : expoly.holes) {
if (!validator.validate_polygon(hole, layer->print_z)) {
support_violations++;
if (support_violations <= 5) { // Log first 5
BOOST_LOG_TRIVIAL(warning) << "Support hole polygon at z=" << layer->print_z
<< " exceeds build volume boundaries";
}
}
}
}
}
if (support_violations > 0) {
BOOST_LOG_TRIVIAL(warning) << "Found " << support_violations
<< " support polygons/paths exceeding build volume boundaries";
// Record violation
ConflictResult violation = ConflictResult::create_boundary_violation(
static_cast<int>(BoundaryValidator::ViolationType::Support),
Vec3d(object.center_offset().x(), object.center_offset().y(), 0.0),
0.0,
object.model_object()->name
);
object.print()->add_boundary_violation(violation);
}
BOOST_LOG_TRIVIAL(info) << "Support generator - End";
}
-94
View File
@@ -2399,100 +2399,6 @@ void GCodeViewer::load_toolpaths(const GCodeProcessorResult& gcode_result, const
{
//BBS: use convex_hull for toolpath outside check
m_contained_in_bed = build_volume.all_paths_inside(gcode_result, m_paths_bounding_box);
// BBS: Enhanced Travel move checking with smart filtering
// Skip initial setup moves (G28, G29) and only check moves during actual printing
if (m_contained_in_bed) {
// Find first extrusion move to determine where actual printing starts
size_t first_print_move = 0;
for (size_t i = 0; i < gcode_result.moves.size(); ++i) {
if (gcode_result.moves[i].type == EMoveType::Extrude &&
gcode_result.moves[i].position.z() > 0.1) { // Above first layer
first_print_move = i;
break;
}
}
// Only check moves after printing starts (skip G28/G29 initialization)
if (first_print_move > 0) {
bool has_travel_violations = false;
int violation_count = 0;
// Use same epsilon as BuildVolume::all_paths_inside() for consistency
static constexpr const double epsilon = BuildVolume::BedEpsilon;
// Clear existing violations and populate with detailed info
auto& gcode_result_writable = const_cast<GCodeProcessorResult&>(gcode_result);
gcode_result_writable.boundary_violations.clear();
for (size_t i = first_print_move; i < gcode_result.moves.size(); ++i) {
const auto& move = gcode_result.moves[i];
// Only check Travel moves (Extrude already checked by all_paths_inside)
if (move.type == EMoveType::Travel) {
// Quick rectangle bed check with tolerance
auto bbox = build_volume.bounding_volume();
if (move.position.x() < bbox.min.x() - epsilon ||
move.position.x() > bbox.max.x() + epsilon ||
move.position.y() < bbox.min.y() - epsilon ||
move.position.y() > bbox.max.y() + epsilon) {
// Create detailed violation info
GCodeProcessorResult::BoundaryViolationInfo violation;
violation.violation_type = BoundaryValidator::ViolationType::TravelMove;
violation.position = Vec3d(move.position.x(), move.position.y(), move.position.z());
violation.print_z = move.position.z();
violation.component_name = "Travel";
// Determine which boundary was exceeded
double dist_x_min = bbox.min.x() - move.position.x();
double dist_x_max = move.position.x() - bbox.max.x();
double dist_y_min = bbox.min.y() - move.position.y();
double dist_y_max = move.position.y() - bbox.max.y();
if (dist_x_min > 0) {
violation.direction = BoundaryValidator::BoundaryDirection::X_Min;
violation.distance_out = dist_x_min;
} else if (dist_x_max > 0) {
violation.direction = BoundaryValidator::BoundaryDirection::X_Max;
violation.distance_out = dist_x_max;
} else if (dist_y_min > 0) {
violation.direction = BoundaryValidator::BoundaryDirection::Y_Min;
violation.distance_out = dist_y_min;
} else if (dist_y_max > 0) {
violation.direction = BoundaryValidator::BoundaryDirection::Y_Max;
violation.distance_out = dist_y_max;
}
gcode_result_writable.boundary_violations.push_back(violation);
violation_count++;
if (violation_count <= 3) { // Log first 3
std::string dir_str;
switch (violation.direction) {
case BoundaryValidator::BoundaryDirection::X_Min: dir_str = "X_min"; break;
case BoundaryValidator::BoundaryDirection::X_Max: dir_str = "X_max"; break;
case BoundaryValidator::BoundaryDirection::Y_Min: dir_str = "Y_min"; break;
case BoundaryValidator::BoundaryDirection::Y_Max: dir_str = "Y_max"; break;
default: dir_str = "Unknown"; break;
}
BOOST_LOG_TRIVIAL(warning) << "Travel move #" << i
<< " outside bounds: " << violation.component_name << " " << dir_str
<< " at pos=(" << move.position.x()
<< ", " << move.position.y() << ", " << move.position.z() << ")";
}
has_travel_violations = true;
}
}
}
if (has_travel_violations) {
BOOST_LOG_TRIVIAL(warning) << "Found " << violation_count
<< " Travel moves outside build volume";
m_contained_in_bed = false;
}
}
}
if (m_contained_in_bed) {
//PartPlateList& partplate_list = wxGetApp().plater()->get_partplate_list();
//PartPlate* plate = partplate_list.get_curr_plate();
-6
View File
@@ -849,12 +849,6 @@ public:
//BBS: add only gcode mode
bool is_only_gcode_in_preview() const { return m_only_gcode_in_preview; }
// Snapmaker: Get boundary violations from gcode_result
const std::vector<GCodeProcessorResult::BoundaryViolationInfo>& get_boundary_violations() const {
static const std::vector<GCodeProcessorResult::BoundaryViolationInfo> empty_violations;
return (m_gcode_result != nullptr) ? m_gcode_result->boundary_violations : empty_violations;
}
EViewType get_view_type() const { return m_view_type; }
void set_view_type(EViewType type, bool reset_feature_type_visible = true) {
if (type == EViewType::Count)
+1 -96
View File
@@ -9687,102 +9687,7 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
}
case EWarning::ObjectOutside: text = _u8L("An object is laid over the plate boundaries."); break;
case EWarning::ToolHeightOutside: text = _u8L("A G-code path goes beyond the max print height."); error = ErrorType::SLICING_ERROR; break;
case EWarning::ToolpathOutside: {
error = ErrorType::SLICING_ERROR;
// Snapmaker: Enhanced boundary violation reporting with detailed information
static std::string prevBoundaryText;
text = prevBoundaryText;
// Helper function to get localized violation type name
auto get_localized_type_string = [](BoundaryValidator::ViolationType type) -> std::string {
switch (type) {
case BoundaryValidator::ViolationType::TravelMove: return _u8L("Travel Move");
case BoundaryValidator::ViolationType::ExtrudeMove: return _u8L("Extrude Move");
case BoundaryValidator::ViolationType::SpiralLift: return _u8L("Spiral Lift");
case BoundaryValidator::ViolationType::LazyLift: return _u8L("Lazy Lift");
case BoundaryValidator::ViolationType::WipeTower: return _u8L("Wipe Tower");
case BoundaryValidator::ViolationType::Skirt: return _u8L("Skirt");
case BoundaryValidator::ViolationType::Brim: return _u8L("Brim");
case BoundaryValidator::ViolationType::Support: return _u8L("Support");
case BoundaryValidator::ViolationType::ArcMove: return _u8L("Arc Move");
default: return _u8L("Unknown");
}
};
// Helper function to get localized direction string
auto get_localized_direction_string = [](BoundaryValidator::BoundaryDirection dir) -> std::string {
switch (dir) {
case BoundaryValidator::BoundaryDirection::X_Min: return _u8L("beyond X minimum");
case BoundaryValidator::BoundaryDirection::X_Max: return _u8L("beyond X maximum");
case BoundaryValidator::BoundaryDirection::Y_Min: return _u8L("beyond Y minimum");
case BoundaryValidator::BoundaryDirection::Y_Max: return _u8L("beyond Y maximum");
case BoundaryValidator::BoundaryDirection::Z_Max: return _u8L("above Z maximum");
case BoundaryValidator::BoundaryDirection::Radius: return _u8L("beyond bed radius");
default: return _u8L("outside boundaries");
}
};
// Try to get detailed violation information from gcode_result
const auto& violations = m_gcode_viewer.get_boundary_violations();
if (!violations.empty()) {
// Group violations by type for better summary
std::map<BoundaryValidator::ViolationType, int> violation_counts;
std::map<BoundaryValidator::ViolationType, std::string> type_names;
for (const auto& v : violations) {
violation_counts[v.violation_type]++;
if (type_names.find(v.violation_type) == type_names.end()) {
type_names[v.violation_type] = get_localized_type_string(v.violation_type);
}
}
// Build detailed message
std::string msg = _u8L("G-code boundary violations detected:\n\n");
int total_count = 0;
for (const auto& [type, count] : violation_counts) {
msg += "" + type_names[type] + ": " + std::to_string(count) + " " + _u8L("violation(s)") + "\n";
total_count += count;
}
if (total_count > 1) {
msg += "\n" + _u8L("Total") + ": " + std::to_string(total_count) + " " + _u8L("violations");
}
// Show details of first few violations
if (!violations.empty()) {
msg += "\n\n" + _u8L("Details") + ":\n";
int show_count = std::min((int)violations.size(), 5);
for (int i = 0; i < show_count; ++i) {
const auto& v = violations[i];
// Build localized description
std::string desc;
if (!v.component_name.empty()) {
desc += v.component_name + " - ";
}
desc += get_localized_type_string(v.violation_type);
desc += " " + get_localized_direction_string(v.direction);
msg += " " + std::to_string(i + 1) + ". " + desc;
if (v.distance_out > 0.001) {
msg += " (" + (boost::format(_u8L("%.2f mm out")) % v.distance_out).str() + ")";
}
if (v.print_z > 0) {
msg += " " + _u8L("at Z") + "=" + (boost::format("%.1f") % v.print_z).str();
}
msg += "\n";
}
if ((int)violations.size() > show_count) {
msg += " " + _u8L("... and more");
}
}
text = msg;
prevBoundaryText = text;
} else {
// Fallback to generic message if no detailed info available
text = _u8L("A G-code path goes beyond the plate boundaries.");
}
break;
}
case EWarning::ToolpathOutside: text = _u8L("A G-code path goes beyond the plate boundaries."); error = ErrorType::SLICING_ERROR; break;
// BBS: remove _u8L() for SLA
case EWarning::SlaSupportsOutside: text = ("SLA supports outside the print area were detected."); error = ErrorType::PLATER_ERROR; break;
case EWarning::SomethingNotShown: text = _u8L("Only the object being edited is visible."); break;
-312
View File
@@ -1,312 +0,0 @@
# 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
View File
@@ -1,449 +0,0 @@
#!/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
View File
@@ -1,674 +0,0 @@
#!/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
View File
@@ -1,28 +0,0 @@
@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
)