# OCCT BRepFill_CompatibleWires 配图使用教程

`BRepFill_CompatibleWires` 用来把一组用于放样的 Wire 调整为彼此兼容的截线。它可以处理遍历方向、闭合轮廓的起点，以及必要的边分段对应。对于两条单独的空间边，可以先把每条 Edge 包成单边 Wire，再调用这个接口。

本教程从两条位置错开、遍历方向相反的空间曲线开始，依次完成建模、调向、读取输出和结果检查。示例已在 OCCT 7.8.1 上编译运行；接口与分支说明另对照官方 V7_9_0 源码。使用其他版本时，以对应版本的头文件和实际结果为准。

需要始终区分：**接口能完成放样截线的兼容处理，但 `IsDone()` 不能作为任意两条空间曲线整段同向的证明。**

## 1 先看接口调整了什么

![两条空间边调用 BRepFill_CompatibleWires 前后对比](assets/curve-orientation-before-after.png)

蓝色边 A 的方向为左端到右端；橙色边 B 初始方向为右端到左端。两条曲线的形状相同，但 B 在空间中平移了一段距离。调用接口后，B 的起点和终点在遍历意义上交换，箭头与 A 一致，曲线的几何形状和位置保持不变。

图中小圆点表示起点，方点表示终点，大圆点表示当前行进位置。虚线只示意相同遍历进度之间的连接关系，不是接口生成的曲面，也不是实际放样后的边界网格。

[打开可独立使用的交互图例](assets/interactive-example.html)。可以切换空间斜视、XY 俯视和 XZ 正视，也可以拖动行进滑块。图例按归一化参数推进，未宣称按弧长匀速；本例两条曲线同形且同参数化，因此便于直接对照。

### 1.1 本例的输入

A 为非平面的三次 Bézier 曲线，四个控制点如下：

| 控制点 | X | Y | Z |
|---|---:|---:|---:|
| P0 | -5.0 | 0.0 | 0.0 |
| P1 | -2.5 | 4.2 | 1.8 |
| P2 | 2.5 | -4.2 | -1.2 |
| P3 | 5.0 | 0.0 | 0.8 |

B 使用相同的控制点形状，整体平移 `(0, 4.6, 3.0)`，再把对应 Edge 设为 `REVERSED`。模型坐标单位由应用约定；接口本身没有给这些数值附加毫米或米的单位。

### 1.2 实际运行结果

| 检查项 | 结果 | 能说明什么 |
|---|---|---|
| `IsDone()` | `true` | 本次兼容处理完成 |
| 输出 `BRepCheck_Analyzer::IsValid()` | 两条均为 `true` | 输出通过该 BRep 有效性检查 |
| A 的 Edge 方向 | `FORWARD → FORWARD` | A 在本例中保持原方向 |
| B 的 Edge 方向 | `REVERSED → FORWARD` | B 被接口反向 |
| 每条 Wire 的边数 | `1 → 1` | 本例没有拆边 |
| 对应输入输出 Edge 的 `IsSame()` | `true` | 本例复用了相同 TShape 和 Location |
| 201 点采样集的最大双向最近距离 | 约 `2.14e-15` | 数值对照中没有可见几何位移 |

[查看原生运行记录](results/native-run.log)，[查看实际采样数据](results/curve-orientation-data.json)。这些结果针对当前这组输入；其他轮廓可能需要改变起点或拆边，不能要求所有调用都保持 Edge 的身份与数量。

## 2 理解 Curve Edge Wire 和方向

### 2.1 输入为什么必须是 Wire

`Geom_Curve` 表示几何曲线，参数递增方向定义了曲线的自然方向。`TopoDS_Edge` 在几何曲线之上增加了边界、参数范围、位置和拓扑方向。`TopoDS_Wire` 则把一条或多条 Edge 组织成可遍历的线框。

这个接口的参数类型虽然是 `TopTools_SequenceOfShape`，其中每个普通元素仍应当是 `TopoDS_Wire`。不要因为容器存储的是 Shape，就直接加入 Edge 或 `Geom_Curve`。[接口声明][s1]

```text
Geom_Curve
    ↓ 构造拓扑边
TopoDS_Edge
    ↓ 包装成单边线框
TopoDS_Wire
    ↓ 按截线顺序加入
TopTools_SequenceOfShape
    ↓
BRepFill_CompatibleWires
    ↓ Shape()
处理后的 Wire 序列
```

### 2.2 FORWARD 和 REVERSED 的含义

对于普通有向 Edge，`FORWARD` 表示按底层曲线参数递增方向遍历，`REVERSED` 表示按参数递减方向遍历。反转拓扑边的方向，通常不需要改变曲线的控制点。

但两条独立 Edge 都是 `FORWARD`，并不保证它们在空间中同向：它们各自的底层曲线可以有相反的参数方向。`Orientation()` 是相对于自身几何表示的标记，不能单独承担两条任意曲线的整体比较。[OCCT 建模数据中的方向概念][s5]

### 2.3 只反转 Wire 和读取输出的区别

不要用输入 Wire 自身的 `Orientation()` 是否改变来判断接口是否调向成功。算法可能重建外层 Wire，并在其中以相反顺序使用反向的 Edge。应当通过 `BRepTools_WireExplorer` 查看输出 Wire 实际包含的有向边及遍历顺序。

## 3 最短调用流程

假设已经有两条有效的 Edge：`edgeA` 和 `edgeB`。

```cpp
#include <BRepFill_CompatibleWires.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <TopTools_SequenceOfShape.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Wire.hxx>

BRepBuilderAPI_MakeWire makeWireA(edgeA);
BRepBuilderAPI_MakeWire makeWireB(edgeB);
if (!makeWireA.IsDone() || !makeWireB.IsDone()) {
    // 输入不能构造成 Wire，先处理输入问题。
    return;
}

TopTools_SequenceOfShape sections;
sections.Append(makeWireA.Wire());
sections.Append(makeWireB.Wire());

BRepFill_CompatibleWires compatible(sections);
compatible.Perform();

if (!compatible.IsDone()) {
    const auto status = compatible.GetStatus();
    // 记录 status，本次不继续使用输出。
    return;
}

// 复制序列，避免持有对算法对象内部序列的长期引用。
TopTools_SequenceOfShape adjusted = compatible.Shape();
TopoDS_Wire outputA = TopoDS::Wire(adjusted.Value(1));
TopoDS_Wire outputB = TopoDS::Wire(adjusted.Value(2));
```

这个片段放在已有的业务函数内使用。它展示正常调用顺序；程序边界仍应处理 `Standard_Failure`。完整程序见[可编译示例](examples/compatible_wires_demo.cxx)。

按以下顺序理解每一步：先把 Edge 组织成 Wire，再执行兼容处理，确认状态，最后采用 `Shape()` 返回的 Wire。后续如果仍使用旧输入，就不会采用接口产生的调整结果。

### 3.1 多条截线应按实际位置顺序加入

对于 `wire0、wire1、wire2`，应按期望的放样推进顺序加入容器。接口不负责自动把一组打乱的空间截线排序。开口 Wire 的方向匹配从第一条有效截线开始，逐步参考前一条已处理的截线；不要假设每条都独立对照最初的第一条。[兼容处理实现][s2]

### 3.2 重新使用对象时先 Init

```cpp
compatible.Init(newSections);
// 如需自定义参数，在 Init 之后设置。
compatible.SetPercent(0.01);
compatible.Perform();
```

重新初始化会清理内部映射，并重设内部参数。不要对同一对象无条件连续调用 `Perform()` 来尝试不同配置；更清楚的做法是重新 `Init`，或者为每次尝试创建一个新对象。

## 4 从几何曲线构造图中的输入

下面的片段解释图例最关键的输入构造。控制点保存在 `TColgp_Array1OfPnt` 中，两个 Bézier 对象使用相同的形状，B 仅增加平移。

```cpp
TColgp_Array1OfPnt polesA(1, 4), polesB(1, 4);
polesA(1) = gp_Pnt(-5.0,  0.0,  0.0);
polesA(2) = gp_Pnt(-2.5,  4.2,  1.8);
polesA(3) = gp_Pnt( 2.5, -4.2, -1.2);
polesA(4) = gp_Pnt( 5.0,  0.0,  0.8);
for (int i = 1; i <= 4; ++i) {
    polesB(i) = polesA(i).Translated(gp_Vec(0.0, 4.6, 3.0));
}

Handle(Geom_BezierCurve) curveA = new Geom_BezierCurve(polesA);
Handle(Geom_BezierCurve) curveB = new Geom_BezierCurve(polesB);

BRepBuilderAPI_MakeEdge makeA(curveA), makeB(curveB);
if (!makeA.IsDone() || !makeB.IsDone()) {
    return;
}
TopoDS_Edge edgeA = makeA.Edge();
TopoDS_Edge edgeB = makeB.Edge();
edgeB.Reverse();
```

最后一行只是在调用前故意准备一个反向输入。**从 `Perform()` 读出结果之后，示例没有再人工反向输出 B。** 因而图中的方向变化来自接口的实际返回结果。

如果已经拿到 `TopoDS_Edge`，直接从第 3 节开始。如果拿到的是裁剪曲线，还需要保证构造 Edge 时采用了正确的有效参数范围；不要把无限直线或无界参数区间直接代入有限曲线的采样逻辑。

## 5 两个参数该怎样设置

### 5.1 Perform 的 WithRotation

```cpp
compatible.Perform(Standard_True);  // 默认调用方式
compatible.Perform(Standard_False); // 不应解读为禁止所有旋转或禁止反向
```

`WithRotation` 参与特定闭合轮廓极坐标匹配分支中的临时坐标比较。源码对比较点做变换，帮助寻找轮廓对应关系；它不是把输出曲线整体搬动或旋转的通用开关。[兼容处理实现][s2]

尤其需要注意：`false` 仍不表示完全不做比较坐标的旋转，也不阻止算法调整遍历方向。开口 Wire 的 `SearchOrigin` 分支没有使用这个参数。图例用的是默认 `Perform()`；改变这个参数，不应被当作修复所有错误调向的办法。

### 5.2 SetPercent 不是全局精度

```cpp
compatible.SetPercent(0.01);
```

这个值用于某些闭合轮廓对应与拆边判断，影响候选位置是否靠近现有端点的参数比例阈值。它不是“允许百分之一的曲线形状误差”，也不是曲线同向比较的角度容差。[接口声明][s1]、[兼容处理实现][s2]

在本次核查版本中，需要区分两种默认行为：

| 写法 | 对应值 |
|---|---:|
| 构造或 `Init` 后不调用 `SetPercent` | 内部初始值为 `0.1` |
| 调用无参 `SetPercent()` | C++ 缺省实参为 `0.01` |
| 调用 `SetPercent(0.02)` | 显式请求 `0.02` |

有效设置范围为 `0 < percent < 1`。超出范围的传参在该实现中被忽略，因此业务层应自行拒绝错误配置。入门示例保持默认行为，以便复现已运行的图例；需要比较参数效果时，应在同一组输入上重新初始化后逐次尝试并检查结果。

## 6 输入约束和失败处理

### 6.1 调用前应检查哪些输入

普通使用场景应保证至少有两条有效截线，每个元素都是非空、连通并且能正确遍历的 Wire；非退化 Edge 应有可用的 3D 曲线。数量、空输入和不合法的拓扑不能指望由该类全面自动诊断。

常规非退化截线应当全部开口或全部闭合。一个开口线框和一个闭合线框混用，会产生拓扑不一致。首尾退化点截线是放样中的特殊情况：底层类用退化 Edge 组成的 Wire 表达，不能直接把 `TopoDS_Vertex` 放进序列。首次集成建议先使用普通截线，点截线优先通过更高层放样 API 的专用接口处理。

输入不要求全部处在同一个平面。当前图例正是非平面曲线；但强对称、退化、自交或很复杂的空间截线仍可能让对应关系不稳定，应在使用前单独验证。

### 6.2 使用 IsDone 和 GetStatus

`GetStatus()` 使用共用的 `BRepFill_ThruSectionErrorStatus` 枚举。[状态定义][s3]

| 状态后缀 | 含义及处理方式 |
|---|---|
| `Done` | 兼容处理完成，继续检查输出 |
| `NotDone` | 尚未成功完成处理 |
| `NotSameTopology` | 检查普通截线是否混合了开口和闭合 |
| `ProfilesInconsistent` | 截线对应不一致，检查轮廓组织方式 |
| `Failed` | 处理失败，保存输入以便定位 |
| `WrongUsage` | 共用枚举中的点截线用法错误状态 |
| `Null3DCurve` | 共用枚举中的缺少 3D 曲线状态 |

共用枚举列出这些值，不表示所有非法输入都一定被本类转换为相应状态；某些问题可能直接抛出 `Standard_Failure`。建议在应用边界保留异常日志和输入 BREP。

### 6.3 一个已运行的失败例子

随教程提供的[拓扑不一致示例](examples/incompatible_topology_demo.cxx)把一个开口线段 Wire 与一个闭合圆 Wire 传给接口，实际输出为：

```text
IsDone=false; NotSameTopology=true
```

这时应停止后续使用，并修正截线组织方式。不要忽略状态后直接取 `Shape()`；该访问函数自身不能代替状态检查。[失败例子的运行记录](results/mixed-topology-run.log)

## 7 如何正确检查输出中的方向

### 7.1 遍历输出 Wire

```cpp
#include <BRepTools_WireExplorer.hxx>

for (BRepTools_WireExplorer it(outputB); it.More(); it.Next()) {
    const TopoDS_Edge& edge = it.Current();
    const TopAbs_Orientation orientation = edge.Orientation();
    // 这里得到的是该边在输出 Wire 中实际使用的方向。
}
```

对于图例这种“输入、输出仍是同一条未拆分 Edge”的情况，可以先确认 `inputEdge.IsSame(outputEdge)`，再比较 `Orientation()` 是否相反。`IsSame()` 不检查方向；`IsEqual()` 还会检查方向。不要把这个局部技巧推广为拆边、多段或任意独立曲线的整体判向方法。

### 7.2 采样和切向必须考虑拓扑方向

`BRepAdaptor_Curve` 处理几何曲线与位置，但使用参数递增采样时仍需自己考虑 Edge 的遍历方向。图例中采用：

```cpp
BRepAdaptor_Curve curve(edge);
double fraction = progress; // 0 到 1
if (edge.Orientation() == TopAbs_REVERSED) {
    fraction = 1.0 - fraction;
}
double u = curve.FirstParameter()
         + fraction * (curve.LastParameter() - curve.FirstParameter());
gp_Pnt point = curve.Value(u);
```

如果还要求沿遍历方向的切向，求 `D1` 后对 `REVERSED` Edge 反转导数向量。尖点或零导数处不能强行构造 `gp_Dir`，应选择有定义的单侧方向，或把该位置标记为不可判定。

对于多段 Wire，还应沿连接次序处理每个分段。本例中的 201 点是绘图采样，不是严格证明整条曲线相同的连续区间检验。

## 8 边被拆分以后怎样找回对应关系

```cpp
#include <TopTools_ListIteratorOfListOfShape.hxx>

const TopTools_ListOfShape& generated = compatible.GeneratedShapes(inputEdge);
for (TopTools_ListIteratorOfListOfShape it(generated); it.More(); it.Next()) {
    TopoDS_Edge piece = TopoDS::Edge(it.Value());
    // piece 是从 inputEdge 派生或沿用的结果边。
}
```

`GeneratedShapes(inputEdge)` 查询一条原边的派生边；`Generated()` 提供整张映射。列表未必只有一条，未绑定的原边可能返回空列表。不要假设列表中存储的方向、顺序，就等于结果 Wire 中的实际遍历方向。

如果需要给结果边着色、标注或关联业务属性，应结合派生映射和输出 Wire 的实际边出现位置进行匹配。算法对象销毁或重新 `Init` 前，要复制后续仍需使用的序列或映射；其中的 Shape 仍按 OCCT 的共享拓扑机制工作，并非自动深拷贝。

## 9 与放样 API 怎样配合

如果最终目的就是生成放样曲面或实体，可以使用 `BRepOffsetAPI_ThruSections` 的兼容处理开关。该高层 API 内部会在相应流程中使用本类。[高层 API 实现][s4]

```cpp
BRepOffsetAPI_ThruSections loft(Standard_False, Standard_False);
loft.CheckCompatibility(Standard_True);
loft.AddWire(wireA);
loft.AddWire(wireB);
loft.Build();
if (!loft.IsDone()) {
    return;
}
TopoDS_Shape result = loft.Shape();
```

这个片段表示高层集成方式，未作为本教程的放样结果样例运行。使用它时仍需检查生成结果、预期边界及是否发生扭转。

如果已经单独调用 `BRepFill_CompatibleWires` 并人工确认输出对应关系，希望后续放样直接采用这些结果，可以在确认版本与业务约束后考虑关闭高层的重复兼容处理；关闭之后，输入兼容性由调用方承担。不要同时把“自动兼容”和“已验证符合具体构造意图”混为一谈。

## 10 针对整体空间走向应额外做什么

本接口解决的是放样截线的兼容问题。开口分支的定向规则仍主要依赖首尾信息；闭合分支通过轮廓点列等信息建立有限的对应。两条任意异位空间曲线若有内部回环、局部反向或形状差异，不能仅凭 `IsDone()` 判定它们全程同向。

如果业务要求的是“沿整段路径的走向是否一致”，应在接口之外定义全路径比较：保持世界坐标朝向、按需要消除平移，覆盖整段自适应采样，分别比较正序和逆序的有序匹配，并检查局部方向和匹配歧义。可以采用保留路径顺序的 Fréchet 匹配作为基础，同时设置几何误差、角度偏差及正反方案差距的阈值。[Fréchet 距离说明][s6]

结果应保留 `同向 / 反向 / 不可判定` 三种状态。两种匹配都差或几乎难以区分时，不强行选较小者。闭合曲线还需处理起点相位，自交曲线需处理多解。这个额外判定方案尚未在本教程中实现，不能把已有的调向图例当作其验证结果。

## 11 编译运行完整示例

### 11.1 文件组织

```text
BRepFill_CompatibleWires-tutorial/
├── tutorial.html
├── tutorial.md
├── assets/
│   ├── curve-orientation-before-after.png
│   └── interactive-example.html
├── examples/
│   ├── CMakeLists.txt
│   ├── compatible_wires_demo.cxx
│   └── incompatible_topology_demo.cxx
└── results/
    ├── native-run.log
    ├── mixed-topology-run.log
    ├── curve-orientation-data.json
    └── 四个输入输出 BREP 文件
```

[下载完整教程与示例包](tutorial-bundle.zip)，或单独查看 [CMake 配置](examples/CMakeLists.txt)。

### 11.2 构建

需要已安装的 OCCT C++ 开发环境、CMake 和支持 C++17 的编译器。在教程目录执行：

```sh
cmake -S examples -B build \
  -DOpenCASCADE_DIR="/path/to/occt/lib/cmake/opencascade" \
  -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel 2
```

把示例路径替换为本机 `OpenCASCADEConfig.cmake` 所在目录。提供的 CMake 配置采用 OCCT 7.8.1 安装导出的 toolkit 目标名；其他安装包可能使用不同的目标命名方式，需要按其导出配置调整。

### 11.3 运行

```sh
./build/compatible_wires_demo run-results
./build/incompatible_topology_demo
```

第一条命令会自动创建 `run-results`，写入采样 JSON 和四个 BREP 文件。Windows 多配置生成器的可执行文件通常位于 `build/Debug/`，并带 `.exe` 后缀；运行时还要保证 OCCT 动态库及其依赖位于程序可搜索的路径。

本次实际构建运行记录如下：

```text
OCCT 7.8.1
IsDone=true, valid=true
A FORWARD -> FORWARD
B REVERSED -> FORWARD
SameEdgeTShapes=true
Edges A 1 -> 1; B 1 -> 1
Demo chord dot=-1 -> 1
Sampled geometry displacement=2.14132e-15
```

`Demo chord dot` 仅用于当前已知平移同形样例的检查，不能移作一般曲线的整体判向逻辑。

### 11.4 最小接入检查

第一次替换为自己的模型时，保存输入与输出 BREP，画出按输出拓扑方向生成的箭头，再检查边数、派生映射、BRep 有效性和预期对应关系。若最终用于放样，还应检查实际生成的曲面或实体；这些检查分别对应不同结果，不能互相替代。

## 12 常见问题

**已经调用成功，画面为什么没有变化？** 可能仍在显示输入 Wire，也可能只画了曲线轮廓而未显示方向。采用 `Shape()` 返回的 Wire，并显示有向箭头。

**两条 Edge 都是 FORWARD，为什么方向仍可能相反？** 两条曲线各自的参数方向不同。枚举值只相对于各自的底层几何定义。

**设置 Perform(false) 后为什么仍发生反向？** 这个参数不禁止调整遍历方向，开口定向分支也不使用它。

**能否只给两条 Edge？** 先各自构造成单边 Wire，再加入序列；不要把 Edge 直接作为序列元素。

**为什么有些结果不再 IsSame？** 算法可能拆边或重建拓扑，应查看派生映射，而不是仅依赖原边身份。

**输出有效，是否表示没有扭转、整体同向？** BRep 有效性、兼容算法状态和符合业务构造意图是不同的检查。需要分别验证。

**能否拿本接口自动修复任何空间曲线的方向？** 适合把预计可对应的放样截线调整为兼容输入；对于任意曲线的全程方向判断，应另外实现明确的判定规则。

## 13 参考资料与版本

- [接口声明 V7_9_0][s1]：输入输出、参数和映射接口。
- [算法实现 V7_9_0][s2]：开闭分支、方向匹配和参数处理。
- [共用错误状态 V7_9_0][s3]：状态枚举。
- [BRepOffsetAPI_ThruSections 实现 V7_9_0][s4]：高层放样与本类的关系。
- [OCCT 建模数据文档][s5]：拓扑方向与几何参数方向。
- [CGAL Fréchet 距离说明][s6]：保持路径顺序的曲线比较背景。

实跑环境为本机 OCCT 7.8.1。图例、成功样例、开闭混合失败样例均已执行；高层放样片段、任意曲线整体判向方案及其他版本行为不在本次运行验证范围内。

[s1]: https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_0/src/BRepFill/BRepFill_CompatibleWires.hxx
[s2]: https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_0/src/BRepFill/BRepFill_CompatibleWires.cxx
[s3]: https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_0/src/BRepFill/BRepFill_ThruSectionErrorStatus.hxx
[s4]: https://github.com/Open-Cascade-SAS/OCCT/blob/V7_9_0/src/BRepOffsetAPI/BRepOffsetAPI_ThruSections.cxx
[s5]: https://occt3d.com/dev/doc/overview/html/occt_user_guides__modeling_data.html
[s6]: https://doc.cgal.org/6.1/Frechet_distance/index.html
