Files
OrcaSlicer/docs/gcode_boundary_checking_optimization.md
xiaoyeliu a1769a2148 Feature boundary test lxy (#128)
* Add Boundary validator

* Boundary test ui

* refect & optimize boundary validation
2026-01-21 19:52:11 +08:00

47 KiB
Raw Blame History

OrcaSlicer G-code超限检测优化技术文档

项目编号: ORCA-2026-001 创建日期: 2026-01-15 作者: Claude Code 状态: 已完成

重要说明:本文档是设计阶段的原始文档。实际实现与设计有一些差异:

  • 原设计在 BuildVolume 中添加 all_moves_inside() 方法
  • 实际实现Travel 检查采用内联方式在 GCodeViewer.cpp 中实现,以便收集详细的违规信息(类型、方向、位置、距离)
  • 这是因为需要返回详细的 BoundaryViolationInfo 结构,而不是简单的布尔值

目录

  1. 项目概述
  2. 当前系统架构分析
  3. G-code生成和验证流程
  4. 问题分析
  5. 优化方案设计
  6. 风险评估
  7. 实施计划
  8. 参考资料

1. 项目概述

1.1 背景

OrcaSlicer 当前存在部分 G-code 路径超出打印边界但未能正确检测和警告的问题,这可能导致:

  • 打印头撞击打印床边界导致硬件损坏
  • 打印失败但用户不知道原因
  • 用户体验差,对软件质量产生质疑

1.2 目标

核心目标:确保所有超出打印边界的 G-code 路径都能被检测并警告用户。

具体目标

  1. 调研并文档化当前 G-code 超限检测逻辑和框架
  2. 识别并修复所有超限切片不报错的场景
  3. 实现警告提示机制(允许继续但高亮警告)
  4. 添加测试用例防止回归

1.3 交付物

  • 完整的技术文档(本文档)
  • 修复所有8个已识别的关键漏洞
  • 单元测试和集成测试用例
  • 代码实现和 Code Review

2. 当前系统架构分析

2.1 整体架构概述

OrcaSlicer 实现了两套独立的超限检测系统

系统 A: 物体边界检测 (Object Bounds Checking)

  • 功能: 检测 3D 模型是否超出打印平台边界
  • 触发时机: 模型放置、移动、导出前
  • 核心类: BuildVolume
  • 检测粒度: 模型级别(基于包围盒和网格)

系统 B: G-code 路径冲突检测 (G-code Conflict Checking)

  • 功能: 检测切片后 G-code 路径是否发生对象间冲突
  • 触发时机: 切片完成后
  • 核心类: ConflictChecker
  • 检测粒度: 路径级别(基于线段相交)

关键发现: 这两套系统各自独立,存在检测盲区。


2.2 核心类和文件结构

2.2.1 冲突检测核心

ConflictChecker (src/libslic3r/GCode/ConflictChecker.hpp/cpp)

struct LineWithID {
    Line          _line;      // 线段几何
    const void *  _id;        // 所属对象指针
    ExtrusionRole _role;      // 挤出角色(支撑/填充/外壁等)
};

struct ConflictChecker {
    static ConflictResultOpt find_inter_of_lines_in_diff_objs(
        PrintObjectPtrs objs,
        std::optional<const FakeWipeTower *> wtdptr
    );
    static ConflictComputeOpt find_inter_of_lines(const LineWithIDs &lines);
    static ConflictComputeOpt line_intersect(const LineWithID &l1, const LineWithID &l2);
};

核心算法

  1. 栅格化加速 (Rasterization):使用 1mm × 1mm 网格将线段映射到空间
  2. 并行检测:使用 TBB 并行处理各层
  3. 早期退出:找到第一个冲突即停止

性能优化

  • 复杂度从 O(n²) 降低到接近 O(n)
  • 使用 3D DDA 算法进行高效栅格化

关键代码位置

  • 栅格化: ConflictChecker.cpp:25-86
  • 冲突检测主函数: ConflictChecker.cpp:220-284
  • 线段相交判定: ConflictChecker.cpp:286-308

2.2.2 打印体积边界检测核心

BuildVolume (src/libslic3r/BuildVolume.hpp/cpp)

enum class BuildVolume_Type {
    Invalid,
    Rectangle,   // 矩形打印床(最常见)
    Circle,      // 圆形打印床Delta 打印机)
    Convex,      // 凸多边形打印床
    Custom       // 自定义形状打印床
};

enum class ObjectState {
    Inside,      // 完全在打印体积内,可打印
    Colliding,   // 与边界碰撞,不可打印
    Outside,     // 完全在打印体积外,被忽略
    Below,       // 完全在打印床下方
};

class BuildVolume {
    ObjectState object_state(const indexed_triangle_set &its,
                             const Transform3f &trafo,
                             bool may_be_below_bed,
                             bool ignore_bottom = true) const;

    bool all_paths_inside(const GCodeProcessorResult& paths,
                         const BoundingBoxf3& paths_bbox,
                         bool ignore_bottom = true) const;
};

关键方法

  • object_state(): 检测物体是否超出打印体积 (BuildVolume.cpp:280-313)
  • all_paths_inside(): 检测 G-code 路径是否在边界内 (BuildVolume.cpp:328-367)

容差配置

  • SceneEpsilon = EPSILON:用于物体检测
  • BedEpsilon = 3 * EPSILON:用于 G-code 检测(更宽松)

2.2.3 主控制流程

Print 类 (src/libslic3r/Print.hpp/cpp)

class Print {
    // 验证方法
    std::string validate() const;                           // Print.cpp:1061
    bool sequential_print_clearance_valid();                 // Print.cpp:560-850

    // 冲突检测结果
    ConflictResultOpt m_conflict_result;                    // Print.hpp:1063

    // 导出 G-code
    std::string export_gcode(const std::string& path,
                            GCodeProcessorResult* result);  // Print.cpp:2224
};

Print 步骤枚举

enum PrintStep {
    psWipeTower,
    psToolOrdering,
    psSkirtBrim,
    psSlicingFinished,
    psGCodeExport,
    psConflictCheck  // ← 冲突检测步骤
};

冲突检测触发 (Print.cpp:2196-2215)

if (!m_no_check && !has_adaptive_layer_height) {
    auto conflictRes = ConflictChecker::find_inter_of_lines_in_diff_objs(
        m_objects, wipe_tower_opt
    );
    m_conflict_result = conflictRes;
    if (conflictRes.has_value()) {
        BOOST_LOG_TRIVIAL(error) << "gcode path conflicts found...";
    }
}

注意:自适应层高时不执行冲突检测(因为 FakeWipeTower 使用固定层高)。

2.2.4 GUI 显示和通知

GLCanvas3D (src/slic3r/GUI/GLCanvas3D.cpp)

enum class EWarning {
    ObjectOutside,         // 物体超出边界
    ToolpathOutside,       // 路径超出边界
    ToolHeightOutside,     // 路径超出最大高度
    GCodeConflict,         // G-code 冲突
    // ...
};

警告显示 (GLCanvas3D.cpp:9666-9681)

case EWarning::GCodeConflict:
    text = (boost::format(
        "Conflicts of G-code paths have been found at layer %d, z = %.2lf mm. "
        "Please separate the conflicted objects farther (%s <-> %s)."
    ) % layer % height % objName1 % objName2).str();

    error = ErrorType::SLICING_SERIOUS_WARNING;

警告级别

  • SLICING_ERROR - 错误(红色)
  • SLICING_SERIOUS_WARNING - 严重警告(橙色)
  • PLATER_WARNING - 普通警告(黄色)

2.3 检测触发时机总览

时间线: 用户操作 → 模型加载 → 切片 → G-code 生成 → 导出

┌─────────────────┬────────────────────────────────────────────────────┐
│  阶段           │  检测内容                                          │
├─────────────────┼────────────────────────────────────────────────────┤
│ 模型加载        │ Model::update_print_volume_state()                 │
│                 │ - 检测模型是否在床内                               │
│                 │ - 使用 BuildVolume::object_state()                 │
├─────────────────┼────────────────────────────────────────────────────┤
│ 模型移动/修改   │ Plater::update_print_volume_state()                │
│                 │ - 实时检测边界状态                                 │
│                 │ - GLCanvas3D::requires_check_outside_state()       │
├─────────────────┼────────────────────────────────────────────────────┤
│ 切片前验证      │ Print::validate()                                  │
│                 │ - 检查打印高度                                     │
│                 │ - 检查挤出头间隙                                   │
│                 │ - 检查床排除区域                                   │
│                 │ - 检查对象碰撞                                     │
├─────────────────┼────────────────────────────────────────────────────┤
│ 切片完成后      │ ConflictChecker::find_inter_of_lines_in_diff_objs()│
│                 │ - 检测对象间路径冲突                               │
│                 │ - 存储到 m_conflict_result                         │
├─────────────────┼────────────────────────────────────────────────────┤
│ G-code 加载     │ GCodeViewer::load()                                │
│                 │ - BuildVolume::all_paths_inside()                  │
│                 │ - 检测挤出路径是否在床内                           │
├─────────────────┼────────────────────────────────────────────────────┤
│ 导出前          │ Plater::get_export_file_path()                     │
│                 │ - 最终边界状态检查                                 │
└─────────────────┴────────────────────────────────────────────────────┘

3. G-code生成和验证流程

3.1 G-code 生成主流程

Print::export_gcode()
  └→ GCode::do_export()
      └→ GCode::_do_export()
          ├→ [初始化阶段]
          │   ├─ 初始化 GCodeProcessor
          │   ├─ 创建 SpiralVase (如启用)
          │   ├─ 创建 PressureEqualizer (如启用)
          │   └─ 创建 SmallAreaInfillCompensator
          │
          ├→ [头部生成]
          │   ├─ 写入头部块
          │   ├─ 生成缩略图
          │   └─ 写入配置块
          │
          └→ [层处理管道] GCode::process_layers()
              └→ 并行处理各层 (TBB)
                  ├─ Stage 1: 生成原始 G-code (process_layer)
                  ├─ Stage 2: 应用螺旋花瓶 (可选)
                  ├─ Stage 3: 应用压力均衡器 (可选)
                  ├─ Stage 4: 应用冷却缓冲 (CoolingBuffer)
                  ├─ Stage 5: 应用自适应 PA 处理器
                  └─ Stage 6: 写入输出流

关键文件

  • 入口: Print.cpp:2224 - Print::export_gcode()
  • 主逻辑: GCode.cpp:1845 - GCode::_do_export()
  • 层处理: GCode.cpp:2770 - GCode::process_layers()
  • 单层处理: GCode.cpp:3619 - GCode::process_layer()

3.2 验证阶段详解

验证点 1: 切片前验证 (Pre-Slicing Validation)

函数: Print::validate() (Print.cpp:1061)

检查项

检查项 代码位置 说明
挤出头间隙验证 Print.cpp:560-850 sequential_print_clearance_valid()
打印高度检查 Print.cpp:1135-1164 验证不超过 printable_height
床排除区域检查 Print.cpp:634-647 get_bed_excluded_area()
对象碰撞检测 Print.cpp:650-680 检查凸包碰撞
多材料兼容性 Print.cpp:1072-1079 check_multi_filament_valid()
螺旋花瓶验证 Print.cpp:1101-1118 单对象/材料约束
擦料塔验证 Print.cpp:1183-1220 喷嘴/耗材直径一致性

验证点 2: G-code 生成时验证 (During Generation)

函数: GCode::travel_to() 及相关

检查项

  • 移动路径验证:needs_retraction() 检查是否需要回抽
  • 避免穿越外壁:AvoidCrossingPerimeters
  • 穿越时回抽:RetractWhenCrossingPerimeters

关键文件

  • src/libslic3r/GCode/AvoidCrossingPerimeters.hpp
  • src/libslic3r/GCode/RetractWhenCrossingPerimeters.hpp

验证点 3: 切片后验证 (Post-Processing)

ConflictChecker (ConflictChecker.cpp:145)

算法流程

1. 收集所有对象的挤出路径
   ├─ getAllLayersExtrusionPathsFromObject()
   ├─ 提取 perimeters 和 support 路径
   └─ 添加 FakeWipeTower 路径(如有)

2. 按层组织线段
   ├─ 使用 LinesBucketQueue 按 Z 高度排序
   ├─ 逐层提取当前高度的所有线段
   └─ 存储到 layersLines 向量

3. 并行检测相交 (TBB)
   ├─ 对每一层调用 find_inter_of_lines()
   ├─ 使用栅格化加速相交检测
   └─ 找到第一个冲突即返回

4. 构造冲突结果
   ├─ 记录冲突对象名称
   ├─ 记录冲突 Z 高度
   └─ 返回 ConflictResult

线段相交判定 (ConflictChecker.cpp:286-308)

// 关键参数
constexpr double SUPPORT_THRESHOLD = 100.0;  // 支撑材料阈值(实际禁用)
constexpr double OTHER_THRESHOLD = 0.01;     // 常规材料阈值

// 判定逻辑
1. 如果两线段属于同一对象  返回无冲突
2. 计算几何相交点
3. 计算相交点到线段端点的最小距离
4. 如果距离 > 阈值  认为发生冲突

验证点 4: GCodeProcessor 验证

函数: GCodeProcessor::process_gcode_line()

检查项

  • 移动命令验证 (G0/G1)
  • 路径超限检测 (toolpath_outside 标志)
  • 床温度验证 (update_slice_warnings())
  • 喷嘴 HRC 验证
  • 时间估算验证(模拟固件加速度限制)

3.3 核心类协作关系

graph TB
    Print[Print 类] --> |切片前验证| Validate[validate 方法]
    Print --> |导出 G-code| GCode[GCode 类]
    GCode --> |生成路径| GCodeWriter[GCodeWriter 类]
    GCodeWriter --> |写入命令| Output[输出流]

    Print --> |切片后检测| ConflictChecker[ConflictChecker]
    ConflictChecker --> |检测结果| ConflictResult[ConflictResult]

    GCode --> |处理 G-code| GCodeProcessor[GCodeProcessor]
    GCodeProcessor --> |路径检测| BuildVolume[BuildVolume]
    BuildVolume --> |边界检查| all_paths_inside[all_paths_inside 方法]

    ConflictResult --> |显示警告| GLCanvas3D[GLCanvas3D]
    all_paths_inside --> |toolpath_outside| GCodeViewer[GCodeViewer]
    GCodeViewer --> |显示警告| GLCanvas3D

4. 问题分析

4.1 已识别的 8 个关键漏洞

漏洞 1: 螺旋抬升超限 (Spiral Lift)

位置: src/libslic3r/GCodeWriter.cpp:547-552

当前代码

if (m_to_lift_type == LiftType::SpiralLift && this->is_current_position_clear()) {
    //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()));
    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");
}

问题描述

  • 代码中已有 TODO 注释说明需要检查弧线路径
  • 螺旋抬升生成 G2/G3 弧线命令,可能超出打印区域
  • 弧线半径 = delta_z / (2 * PI * tan(slope))当Z抬升较大时半径可能很大

影响范围

  • 使用螺旋抬升功能的所有打印
  • 特别是物体靠近床边缘时

风险等级: (高风险)


漏洞 2: 懒惰抬升超限 (Lazy Lift Slope)

位置: src/libslic3r/GCodeWriter.cpp:555-568

当前代码

else if (m_to_lift_type == LiftType::LazyLift && ...) {
    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;
    // 直接生成 G-code没有边界检查
    GCodeG1Formatter w0;
    w0.emit_xyz(slope_top_point);
    w0.emit_f(travel_speed * 60.0);
    slop_move = w0.string();
}

问题描述

  • 计算斜坡顶点位置但不验证是否在边界内
  • slope_top_point 可能超出打印床

影响范围

  • 使用懒惰抬升功能的打印
  • 长距离 travel 移动时风险更大

风险等级: (中高风险)


漏洞 3: 擦料塔位置超限 (Wipe Tower)

位置: src/libslic3r/Print.cpp:943-977

当前代码

float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
float width = config.prime_tower_width.value;

// 只检查与对象和排除区域的碰撞
// 没有检查擦料塔本身是否在床边界内!
Polygon wipe_tower_convex_hull = /* ... */;
if (intersects(wipe_tower_convex_hull, object_convex_hull)) {
    // 报错
}

问题描述

  • 只检查擦料塔与其他物体的碰撞
  • 不检查擦料塔本身是否超出床边界
  • 擦料塔包括 brim 和稳定锥,实际占用面积大于配置的宽度

影响范围

  • 所有多材料打印(使用擦料塔)
  • 用户手动设置擦料塔位置时

风险等级: (极高风险 - 应为阻断性错误)


漏洞 4: 裙边超限 (Skirt)

位置: src/libslic3r/Print.cpp:2338-2357

当前代码

distance += float(scale_(spacing));
Polygons loops = offset(convex_hull, distance, ClipperLib::jtRound, float(scale_(0.1)));

// 没有边界检查!
// 直接创建挤出路径

问题描述

  • 通过偏移凸包生成裙边
  • 不验证裙边是否超出床边界
  • 大物体 + 大裙边距离 = 必超限

影响范围

  • 使用裙边功能的打印(很常见)
  • 大物体接近床边缘时

风险等级: (中高风险)


漏洞 5: 边缘超限 (Brim)

位置: src/libslic3r/Brim.cpp

问题描述

  • Brim 生成后不验证边界
  • 类似裙边问题
  • Brim 通常比 Skirt 更宽

影响范围

  • 使用 Brim 功能的打印
  • 提高附着力时常用

风险等级: (中高风险)


漏洞 6: 支撑材料超限 (Support Material)

位置: src/libslic3r/SupportMaterial.cpp, src/libslic3r/Support/TreeSupport.cpp

问题描述

  • 支撑材料自动生成算法
  • 没有明确的边界验证步骤
  • 树形支撑可能延伸到模型外很远

影响范围

  • 使用支撑材料的打印
  • 特别是树形支撑

风险等级: (中风险)


漏洞 7: Travel Moves 不验证 ⚠️ 严重

位置: src/libslic3r/BuildVolume.cpp:328-367

当前代码

bool BuildVolume::all_paths_inside(...) const {
    auto move_valid = [](const GCodeProcessorResult::MoveVertex &move) {
        // 只检查挤出移动!
        return move.type == EMoveType::Extrude &&
               move.extrusion_role != erCustom &&
               move.width != 0.f &&
               move.height != 0.f;
    };

    // 所有 Travel 移动都被跳过验证
    return std::all_of(paths.moves.begin(), paths.moves.end(),
        [move_valid, ...](const GCodeProcessorResult::MoveVertex &move) {
            return !move_valid(move) || /* 边界检查 */;
        });
}

问题描述

  • 只验证挤出移动 (Extrude),不验证 Travel 移动
  • Travel 移动可能超出边界导致撞机
  • 这是一个设计缺陷不是实现bug

影响范围

  • 所有打印的所有 Travel 移动
  • 影响面最广

风险等级: (极高风险 - 系统性缺陷)


漏洞 8: 弧线路径超限 (Arc G2/G3)

位置: src/libslic3r/GCodeWriter.cpp:673-691, 732-752

当前代码

std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d& ij_offset, ...) {
    // 生成 G2/G3 弧线命令
    // 只检查端点,不检查弧线路径上的中间点
    GCodeG2G3Formatter w;
    w.emit_ij(ij_offset);
    // ...
}

问题描述

  • 弧线命令只验证端点
  • 弧线路径上的中间点可能超出边界
  • 适用于螺旋抬升和弧形挤出

影响范围

  • 所有使用弧线命令的功能
  • ArcWelder 功能

风险等级: (中高风险)


4.2 根本原因分析

原因 1: 缺乏统一的边界验证接口

问题

  • 边界检测逻辑分散在多个类中
  • 每个功能模块独立实现(或不实现)边界检查
  • 没有强制的边界验证规范

影响

  • 新功能容易忘记添加边界检查
  • 难以维护和审查

原因 2: Travel Moves 被排除在验证之外

问题

  • all_paths_inside() 设计时只考虑挤出路径
  • 假设 Travel 移动不重要(错误假设)

影响

  • 系统性漏洞,影响面最广

原因 3: 特殊路径生成缺少验证步骤

问题

  • 螺旋抬升、懒惰抬升等特殊功能
  • 直接生成 G-code绕过了验证流程

影响

  • 这些功能成为"盲区"

原因 4: 弧线路径只检查端点

问题

  • 弧线是曲线,端点在边界内不代表整条弧线在边界内
  • 缺少弧线采样验证

影响

  • 弧线相关功能存在风险

原因 5: 几何计算与验证分离

问题

  • 先计算几何(偏移、弧线等)
  • 后生成路径
  • 中间没有验证步骤

影响

  • 很多"计算完就直接用"的场景

4.3 影响分析矩阵

漏洞 触发频率 严重程度 用户感知 风险等级 修复优先级
Travel Moves 不验证 极高 极高 P0
擦料塔位置超限 极高 P0
螺旋抬升超限 P1
Brim 超限 P1
Skirt 超限 P1
弧线路径超限 P2
懒惰抬升超限 P2
支撑材料超限 P2

优先级定义

  • P0: 立即修复(极高风险)
  • P1: 高优先级(高风险)
  • P2: 中等优先级(中风险)

5. 优化方案设计

5.1 核心设计原则

  1. 统一边界检测接口: 创建 BoundaryValidator 抽象层
  2. 分层验证: 在多个阶段验证路径生成时、G-code生成后、最终输出前
  3. 类型化警告系统: 扩展 ConflictResult 支持多种超限类型
  4. 非侵入式修复: 尽量通过扩展而非修改核心逻辑
  5. 性能优先: 使用采样而非详尽检查,避免影响切片速度

5.2 架构设计

5.2.1 新增 BoundaryValidator 抽象类

文件: src/libslic3r/BoundaryValidator.hpp (新建)

namespace Slic3r {

class BoundaryValidator {
public:
    enum class ViolationType {
        SpiralLiftOutOfBounds,
        LazyLiftOutOfBounds,
        WipeTowerOutOfBounds,
        SkirtOutOfBounds,
        BrimOutOfBounds,
        SupportOutOfBounds,
        TravelMoveOutOfBounds,
        ArcPathOutOfBounds
    };

    struct BoundaryViolation {
        ViolationType type;
        std::string description;
        Vec3d position;          // 超限位置
        double layer_z;          // Z高度
        std::string object_name; // 相关对象名称
    };

    using BoundaryViolations = std::vector<BoundaryViolation>;

    virtual ~BoundaryValidator() = default;

    // 核心验证方法
    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 Vec3d& center, double radius,
                              double start_angle, double end_angle,
                              double z_height) const = 0;
    virtual bool validate_polygon(const Polygon& poly, double z_height = 0.0) const = 0;
};

// 基于 BuildVolume 的具体实现
class BuildVolumeBoundaryValidator : public BoundaryValidator {
    const BuildVolume& m_build_volume;
    double m_epsilon;

public:
    BuildVolumeBoundaryValidator(const BuildVolume& bv,
                                 double epsilon = BuildVolume::BedEpsilon)
        : m_build_volume(bv), m_epsilon(epsilon) {}

    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:
    // 弧线采样在弧线上采样N个点进行验证
    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;
};

} // namespace Slic3r

设计要点

  • 抽象接口便于未来扩展(例如自定义验证逻辑)
  • 提供点、线、弧、多边形四种基本验证
  • 弧线验证使用采样方法默认16个采样点
  • 可配置 epsilon 容差

5.2.2 扩展 ConflictResult

文件: src/libslic3r/GCode/GCodeProcessor.hpp (修改)

struct ConflictResult {
    // ===== 现有字段 =====
    std::string  _objName1;
    std::string  _objName2;
    double       _height;
    const void * _obj1;
    const void * _obj2;
    int          layer;

    // ===== 新增字段 =====
    enum class ConflictType {
        ObjectCollision,      // 对象间冲突(原有)
        BoundaryViolation     // 边界超限(新增)
    };

    ConflictType conflict_type = ConflictType::ObjectCollision;

    // 仅当 conflict_type == BoundaryViolation 时有效
    BoundaryValidator::ViolationType violation_type;
    Vec3d violation_position;

    // 构造函数
    ConflictResult() = default;

    // 对象冲突构造函数(保持兼容)
    ConflictResult(const void* o1, const void* o2, double h)
        : _obj1(o1), _obj2(o2), _height(h),
          conflict_type(ConflictType::ObjectCollision) {}

    // 边界超限构造函数(新增)
    static ConflictResult create_boundary_violation(
        BoundaryValidator::ViolationType type,
        const Vec3d& pos,
        double height,
        const std::string& obj_name = ""
    ) {
        ConflictResult result;
        result.conflict_type = ConflictType::BoundaryViolation;
        result.violation_type = type;
        result.violation_position = pos;
        result._height = height;
        result._objName1 = obj_name;
        result.layer = -1;  // 稍后计算
        return result;
    }
};

5.2.3 在 Print 类中添加边界超限收集

文件: src/libslic3r/Print.hpp (修改)

class Print {
    // ===== 现有成员 =====
    ConflictResultOpt m_conflict_result;

    // ===== 新增成员 =====
    std::vector<ConflictResult> m_boundary_violations;

public:
    // 新增方法
    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();
    }
};

5.3 具体修复方案

修复方案 1: Travel Moves 验证 (P0)

实际实现说明:设计方案中提出了在 BuildVolume 中添加 all_moves_inside() 方法,但最终实现采用了不同的方式。

实际实现位置: src/slic3r/GUI/GCodeViewer.cpp:2427-2477

实现方式: 内联检查(而非调用 BuildVolume 方法)

为什么采用内联实现:

  • 需要收集详细的违规信息类型、方向、位置、距离、Z高度
  • 简单的布尔返回值无法提供诊断数据
  • 内联实现可以直接填充 BoundaryViolationInfo 结构

设计方案(未采用): 以下是原始设计方案,供参考

文件: src/libslic3r/BuildVolume.hpp/cpp (设计方案,未实施)

新增方法: all_moves_inside() (检查所有移动,包括 Travel)

// BuildVolume.hpp
bool all_moves_inside(const GCodeProcessorResult& paths,
                     const BoundingBoxf3& paths_bbox,
                     bool ignore_bottom = true) const;

// BuildVolume.cpp
bool BuildVolume::all_moves_inside(const GCodeProcessorResult& paths,
                                   const BoundingBoxf3& paths_bbox,
                                   bool ignore_bottom) const
{
    auto move_significant = [](const GCodeProcessorResult::MoveVertex &move) {
        // 验证所有移动,排除回抽/反回抽
        return move.type != EMoveType::Retract &&
               move.type != EMoveType::Unretract;
    };

    static constexpr const double epsilon = BedEpsilon;

    switch (m_type) {
    case BuildVolume_Type::Rectangle:
    {
        BoundingBox3Base<Vec3d> build_volume = this->bounding_volume().inflated(epsilon);
        if (m_max_print_height == 0.0)
            build_volume.max.z() = std::numeric_limits<double>::max();
        if (ignore_bottom)
            build_volume.min.z() = -std::numeric_limits<double>::max();

        return std::all_of(paths.moves.begin(), paths.moves.end(),
            [move_significant, &build_volume](const GCodeProcessorResult::MoveVertex &move) {
                return !move_significant(move) || build_volume.contains(move.position);
            });
    }
    case BuildVolume_Type::Circle:
    {
        const Vec2f c = unscaled<float>(m_circle.center);
        const float r = unscaled<double>(m_circle.radius) + epsilon;
        const float r2 = sqr(r);
        return m_max_print_height == 0.0 ?
            std::all_of(paths.moves.begin(), paths.moves.end(),
                [move_significant, c, r2](const GCodeProcessorResult::MoveVertex &move) {
                    return !move_significant(move) ||
                           (to_2d(move.position) - c).squaredNorm() <= r2;
                }) :
            std::all_of(paths.moves.begin(), paths.moves.end(),
                [move_significant, c, r2, z = m_max_print_height + epsilon]
                (const GCodeProcessorResult::MoveVertex& move) {
                    return !move_significant(move) ||
                           ((to_2d(move.position) - c).squaredNorm() <= r2 &&
                            move.position.z() <= z);
                });
    }
    case BuildVolume_Type::Convex:
    case BuildVolume_Type::Custom:
        return m_max_print_height == 0.0 ?
            std::all_of(paths.moves.begin(), paths.moves.end(),
                [move_significant, this](const GCodeProcessorResult::MoveVertex &move) {
                    return !move_significant(move) ||
                           Geometry::inside_convex_polygon(
                               m_top_bottom_convex_hull_decomposition_bed,
                               to_2d(move.position).cast<double>());
                }) :
            std::all_of(paths.moves.begin(), paths.moves.end(),
                [move_significant, this, z = m_max_print_height + epsilon]
                (const GCodeProcessorResult::MoveVertex &move) {
                    return !move_significant(move) ||
                           (Geometry::inside_convex_polygon(
                                m_top_bottom_convex_hull_decomposition_bed,
                                to_2d(move.position).cast<double>()) &&
                            move.position.z() <= z);
                });
    default:
        return true;
    }
}

调用位置: GCodeViewer::load() 中添加调用

// GCodeViewer.cpp
if (!build_volume.all_moves_inside(gcode_result, paths_bbox)) {
    m_toolpath_outside = true;
    // 记录具体的超限移动
    for (const auto& move : gcode_result.moves) {
        if ((move.type == EMoveType::Travel || move.type == EMoveType::Extrude) &&
            !build_volume.contains(move.position)) {
            // 记录超限位置
        }
    }
}

修复方案 2: 擦料塔位置验证 (P0)

文件: src/libslic3r/Print.cpp

修改位置: Print::validate() 方法

// Print.cpp - validate() 方法中添加
if (config.prime_tower_width > 0) {
    const size_t plate_index = 0;  // 获取当前板索引
    float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
    float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
    float width = config.prime_tower_width.value;
    float brim_width = config.prime_tower_brim_width.value;

    // 构造擦料塔多边形(包括 brim
    float total_width = width + 2 * brim_width;
    Polygon wipe_tower_polygon;
    wipe_tower_polygon.points.push_back(Point(scale_(x), scale_(y)));
    wipe_tower_polygon.points.push_back(Point(scale_(x + total_width), scale_(y)));
    wipe_tower_polygon.points.push_back(Point(scale_(x + total_width), scale_(y + total_width)));
    wipe_tower_polygon.points.push_back(Point(scale_(x), scale_(y + total_width)));

    // 验证擦料塔是否在床边界内
    BuildVolumeBoundaryValidator validator(this->build_volume());
    if (!validator.validate_polygon(wipe_tower_polygon, 0.0)) {
        throw Slic3r::SlicingError(
            (boost::format(
                "The wipe tower at position (%.2f, %.2f) with width %.2f "
                "(including %.2f mm brim) exceeds the bed boundaries. "
                "Please adjust the wipe tower position in the configuration."
            ) % x % y % total_width % brim_width).str()
        );
    }

    // 还需要检查擦料塔是否与床排除区域冲突
    // ... (现有逻辑保持)
}

注意: 这是阻断性错误,不允许切片继续。


修复方案 3: 螺旋抬升验证 (P1)

文件: src/libslic3r/GCodeWriter.cpp

前提: GCodeWriter 需要访问 BoundaryValidator

// GCodeWriter.hpp 添加成员
class GCodeWriter {
    const BoundaryValidator* m_boundary_validator = nullptr;
    std::vector<BoundaryValidator::BoundaryViolation>* m_violations = nullptr;

public:
    void set_boundary_validator(const BoundaryValidator* validator,
                                std::vector<BoundaryValidator::BoundaryViolation>* violations) {
        m_boundary_validator = validator;
        m_violations = violations;
    }
};

修改 travel_to_z() 方法:

// GCodeWriter.cpp:545-575
std::string GCodeWriter::travel_to_z(double z, const std::string& comment) {
    // ... 现有代码 ...

    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()));
            Vec2d ij_offset = radius * delta_no_z.normalized();
            ij_offset = { -ij_offset(1), ij_offset(0) };

            // ===== 新增:边界验证 =====
            if (m_boundary_validator) {
                Vec3d arc_center = source + Vec3d(ij_offset.x(), ij_offset.y(), 0);
                double start_angle = atan2(-ij_offset.y(), -ij_offset.x());
                double end_angle = start_angle + 2 * PI;

                if (!m_boundary_validator->validate_arc(arc_center, radius,
                                                        start_angle, end_angle, source.z())) {
                    // 记录超限警告
                    if (m_violations) {
                        m_violations->push_back({
                            BoundaryValidator::ViolationType::SpiralLiftOutOfBounds,
                            "Spiral lift arc exceeds bed boundaries",
                            arc_center,
                            source.z(),
                            ""
                        });
                    }
                    // 降级为 LazyLift
                    BOOST_LOG_TRIVIAL(warning) << "Spiral lift exceeds boundaries, falling back to lazy lift";
                    m_to_lift_type = LiftType::LazyLift;
                    // 重新执行(会进入 LazyLift 分支)
                    return this->travel_to_z(z, comment);
                }
            }

            slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
        }
        else if (m_to_lift_type == LiftType::LazyLift && ...) {
            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;

            // ===== 新增:边界验证 =====
            if (m_boundary_validator &&
                !m_boundary_validator->validate_point(slope_top_point)) {
                // 记录超限警告
                if (m_violations) {
                    m_violations->push_back({
                        BoundaryValidator::ViolationType::LazyLiftOutOfBounds,
                        "Lazy lift slope exceeds bed boundaries",
                        slope_top_point,
                        source.z(),
                        ""
                    });
                }
                // 降级为 NormalLift
                BOOST_LOG_TRIVIAL(warning) << "Lazy lift exceeds boundaries, falling back to normal lift";
                slop_move = _travel_to_z(target.z(), "normal lift Z (fallback)");
            } 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();
            }
        }
        // ... 其他分支 ...
    }

    // ... 现有代码 ...
}

其他修复方案摘要

Skirt/Brim/Support: 类似策略,在生成后添加验证,记录警告但不阻断。

Arc 路径: 在 _spiral_travel_to_z()extrude_arc_to_xy() 中使用 validate_arc()


6. 风险评估

6.1 技术风险

风险 描述 影响 缓解措施 优先级
性能影响 增加边界检测可能拖慢切片 用户体验下降 使用采样而非详尽检查、缓存结果
误报 过于严格导致正常打印也报警 用户信任度下降 合理设置 epsilon、充分测试
兼容性 修改可能影响现有功能 功能回归 充分的回归测试
复杂性 新增抽象层增加代码复杂度 维护成本上升 清晰的文档和注释

6.2 实施风险

风险 描述 影响 缓解措施 优先级
擦料塔阻断性错误 改为阻断可能影响现有用户 用户不满 提供清晰的错误提示和修复建议
测试覆盖不足 边界条件难以穷举 隐藏 bug 收集用户反馈、持续改进
回归 bug 修改核心逻辑导致新bug 功能损坏 严格的 Code Review

7. 实施计划

7.1 里程碑规划

里程碑 1: 基础设施 (1 周)

  • 创建 BoundaryValidator
  • 扩展 ConflictResult
  • 添加基础单元测试

里程碑 2: P0 修复 (1 周)

  • 修复 Travel Moves 验证
  • 修复擦料塔位置验证
  • 集成测试

里程碑 3: P1 修复 (1 周)

  • 修复螺旋抬升
  • 修复 Skirt/Brim
  • 集成测试

里程碑 4: P2 修复和 GUI (1 周)

  • 修复剩余问题
  • GUI 警告系统
  • 可视化

里程碑 5: 文档和收尾 (0.5 周)

  • 完善技术文档
  • 回归测试
  • Code Review

总计: 约 4.5 周


7.2 详细任务清单

阶段 1: 基础设施

  • 创建 src/libslic3r/BoundaryValidator.hpp
  • 实现 BuildVolumeBoundaryValidator
  • 扩展 ConflictResult 结构
  • Print 类添加 m_boundary_violations
  • 编写单元测试 test_boundary_validator.cpp

阶段 2: P0 修复(关键风险)

  • 实现 BuildVolume::all_moves_inside()
  • GCodeViewer::load() 中调用验证
  • 实现擦料塔位置验证(阻断性错误)
  • 测试 P0 修复

阶段 3: P1 修复

  • 修复螺旋抬升边界检查
  • 修复懒惰抬升边界检查
  • 修复 Skirt 边界检查
  • 修复 Brim 边界检查
  • 测试 P1 修复

阶段 4: P2 修复和 GUI

  • 修复弧线路径验证
  • 修复支撑材料验证
  • 扩展 GUI 警告枚举
  • 实现警告文本生成
  • 可视化超限区域

阶段 5: 测试和文档

  • 集成测试场景
  • 回归测试
  • 性能测试
  • 更新本技术文档
  • Code Review

7.3 成功标准

  1. 所有 8 个漏洞都已修复
  2. 单元测试覆盖率 > 85%
  3. 集成测试通过率 100%
  4. 性能影响 < 5% (切片时间)
  5. 零严重回归 bug
  6. Code Review 通过
  7. 技术文档完整

8. 参考资料

8.1 关键代码文件索引

功能 文件路径
冲突检测 src/libslic3r/GCode/ConflictChecker.hpp/cpp
边界检测 src/libslic3r/BuildVolume.hpp/cpp
G-code生成 src/libslic3r/GCode.hpp/cpp
G-code写入 src/libslic3r/GCodeWriter.hpp/cpp
G-code处理 src/libslic3r/GCode/GCodeProcessor.hpp/cpp
打印控制 src/libslic3r/Print.hpp/cpp
GUI警告 src/slic3r/GUI/GLCanvas3D.cpp
G-code预览 src/slic3r/GUI/GCodeViewer.cpp

8.2 相关概念

  • BuildVolume: 打印体积,定义打印边界
  • ConflictChecker: 冲突检测器,检测对象间路径冲突
  • GCodeProcessor: G-code 处理器,解析和验证 G-code
  • Spiral Lift: 螺旋抬升,使用弧线命令抬升 Z 轴
  • Lazy Lift: 懒惰抬升,使用斜坡移动抬升 Z 轴
  • Wipe Tower: 擦料塔,多材料打印时的清料塔
  • Skirt: 裙边,打印对象周围的轮廓线
  • Brim: 边缘,增加附着力的边缘结构

8.3 相关 Issue 和 PR

(待补充:相关的 GitHub Issue 和 Pull Request 链接)


附录 A: 架构流程图

A.1 当前验证流程

用户操作
   │
   ├─→ 加载模型
   │      └─→ BuildVolume::object_state() ✓
   │
   ├─→ 移动模型
   │      └─→ Plater::update_print_volume_state() ✓
   │
   ├─→ 开始切片
   │      ├─→ Print::validate() ✓
   │      │      ├─ 打印高度检查
   │      │      ├─ 挤出头间隙检查
   │      │      └─ 擦料塔验证 ✗ (漏洞3)
   │      │
   │      ├─→ 生成路径
   │      │      ├─ Skirt ✗ (漏洞4)
   │      │      ├─ Brim ✗ (漏洞5)
   │      │      └─ Support ✗ (漏洞6)
   │      │
   │      └─→ 生成 G-code
   │             ├─ Spiral Lift ✗ (漏洞1)
   │             ├─ Lazy Lift ✗ (漏洞2)
   │             └─ Arc Path ✗ (漏洞8)
   │
   ├─→ 切片完成
   │      └─→ ConflictChecker::find_inter_of_lines_in_diff_objs() ✓
   │
   └─→ 加载预览
          └─→ BuildVolume::all_paths_inside() ✗ (漏洞7 - 不检查Travel)

图例: ✓ 有检测  ✗ 无检测/不完整

A.2 优化后验证流程

用户操作
   │
   ├─→ 加载模型
   │      └─→ BuildVolume::object_state() ✓
   │
   ├─→ 移动模型
   │      └─→ Plater::update_print_volume_state() ✓
   │
   ├─→ 开始切片
   │      ├─→ Print::validate() ✓✓
   │      │      ├─ 打印高度检查
   │      │      ├─ 挤出头间隙检查
   │      │      └─ 擦料塔边界验证 ✓✓ (NEW - 阻断性)
   │      │
   │      ├─→ 生成路径
   │      │      ├─ Skirt + BoundaryValidator ✓✓ (NEW)
   │      │      ├─ Brim + BoundaryValidator ✓✓ (NEW)
   │      │      └─ Support + BoundaryValidator ✓✓ (NEW)
   │      │
   │      └─→ 生成 G-code
   │             ├─ Spiral Lift + Arc Validator ✓✓ (NEW)
   │             ├─ Lazy Lift + Point Validator ✓✓ (NEW)
   │             └─ Arc Path + Arc Validator ✓✓ (NEW)
   │
   ├─→ 切片完成
   │      └─→ ConflictChecker::find_inter_of_lines_in_diff_objs() ✓
   │
   └─→ 加载预览
          └─→ GCodeViewer: 内联 Travel 检查 ✓✓ (NEW - 收集详细违规信息)

图例: ✓ 原有检测  ✓✓ 新增/增强检测

> **实际实现说明**Travel 检查采用内联实现而非 BuildVolume 方法,以便收集详细的违规信息(类型、方向、位置、距离)。见 `GCodeViewer.cpp:2427-2477`。

附录 B: 测试场景矩阵

场景ID 描述 触发条件 预期结果 优先级
T1 大物体 + 大Skirt 物体195x195, Skirt距离10mm, 床200x200 警告Skirt超限 P0
T2 擦料塔在床外 手动设置塔位置(210, 210), 床200x200 错误:阻止切片 P0
T3 螺旋抬升超限 物体在(195,0), Spiral Lift, 床200x200 警告降级为LazyLift P1
T4 Travel移动超限 多物体Travel路径超出边界 警告Travel超限 P0
T5 Brim超限 物体190x190 + 15mm Brim, 床200x200 警告Brim超限 P1
T6 支撑超限 悬垂模型,支撑延伸超出床 警告Support超限 P2
T7 弧线挤出超限 ArcWelder + 边缘物体 警告Arc超限 P2
T8 正常打印 所有路径在床内 无警告 P0

附录 C: API 变更清单

新增类

// BoundaryValidator.hpp
class BoundaryValidator;
class BuildVolumeBoundaryValidator;

// BoundaryValidator.hpp
enum class ViolationType { ... };
struct BoundaryViolation { ... };

新增方法

实际实现说明BuildVolume::all_moves_inside() 未添加。Travel 检查采用内联实现。

// BuildVolume.hpp - 无变更 (设计方案未采纳)

// GCodeViewer.cpp - 内联 Travel 检查实现
// 位置: lines 2427-2477
// 功能: 检查 Travel 移动并填充 boundary_violations

// Print.hpp (如实施)
void Print::add_boundary_violation(const ConflictResult&);
const std::vector<ConflictResult>& Print::get_boundary_violations() const;
void Print::clear_boundary_violations();

// GCodeWriter.hpp (如实施)
void GCodeWriter::set_boundary_validator(...);

修改结构

// GCodeProcessor.hpp
struct ConflictResult {
    // 新增字段
    ConflictType conflict_type;
    ViolationType violation_type;
    Vec3d violation_position;

    // 新增静态方法
    static ConflictResult create_boundary_violation(...);
};

文档版本: v1.0 最后更新: 2026-01-15 作者: Claude Code