Technical note
OCCT 局部补面
记录一次 CAD/CAE 局部补面的工程思路:从用户选择几条边开始,如何构造 wire、判断边界、生成修补面,并通过 Sewing、验证和局部替换把补面结果安全放回模型。
背景
在几何修复里,“补面”是一个很容易被低估的功能。
从用户角度看,它似乎很直观:模型上缺了一块面,选中周围几条边,系统生成一张新面,把洞补上。
从 API 角度看,也好像不复杂:构造一个 TopoDS_Wire,然后用 BRepBuilderAPI_MakeFace 或 BRepFill_Filling 创建 TopoDS_Face。
但真正放到 CAD/CAE 工程里,补面并不是一个简单的 makeFace()。
因为从“用户选择几条边”到“模型被正确修复”,中间至少要处理这些问题:
用户选中的边是否属于同一个边界;
这些边能不能排序成连续 wire;
wire 是否闭合;
边方向是否一致;
边界是否共面;
非共面边界应该如何生成曲面;
生成的新面质量是否可接受;
新面是否和周围旧面拓扑连接;
修复后 free edge 是否减少;
局部结果如何替换回原 shape;
修复失败后如何回滚。
所以补面不是单个 OCCT API 的调用,而是一条完整的局部修复流程。
这篇文章主要记录从几条边创建修补面时,我认为比较关键的工程步骤和边界。
选边只是线索
交互式补面通常从用户选择边开始。
用户可能在视图里点选几条 free edge,也可能框选一圈边界,希望系统把中间缺失的面补上。
但用户选择的边只是输入线索,不是可以直接补面的边界。
例如用户选择结果可能存在这些情况:
少选了一条边;
多选了不相关的边;
边顺序是乱的;
几条边不连续;
边之间有微小 gap;
边方向不一致;
边界不是闭合 loop;
边界存在自交;
这些边并不属于同一个局部问题。
如果直接把这些 edge 丢给 BRepFill_Filling,失败是很正常的。更麻烦的是,有时候 API 可能生成了一个结果,但这个结果并不符合用户想要的修补面。
所以补面流程的第一步不是生成面,而是把用户选择转换成一个可靠的边界描述。
可以理解成:
Selected Edges
↓
Boundary Analysis
↓
TopoDS_Wire
↓
Patch Face
中间的 Boundary Analysis 才是补面能否稳定的关键。
先判断是不是同一个边界
用户选择的边,首先要做聚类。
如果几条边相互之间没有拓扑或几何连续关系,它们不应该被强行组成一个 wire。
例如用户框选时可能同时选到两个洞的边界,或者选到一个洞边界和旁边的普通特征边。
工程上可以先做几类检查:
边是否来自同一个 shape 或局部 patch;
边端点之间是否能形成连接关系;
是否存在多个不连通边界;
边界是否能聚成一个 chain 或 loop;
是否存在明显离群边。
如果检测到多个不连通边界,更好的做法是让用户重新选择,或者把它们拆成多个补面候选,而不是一次性补成一张面。
这一步也可以和 free edge 检测结合。
如果用户选择的是自由边,并且这些自由边能形成一个闭合 loop,那么补面的可能性会更高。
如果用户选择的边不是 free edge,而是模型内部共享边,就要谨慎判断:用户到底是想补面,还是想用这些边生成一张辅助面。
交互式修复里,系统应该给出明确反馈。比如:
当前选择无法形成连续边界;
当前选择包含多个独立边界;
当前边界未闭合;
当前边界可能适合补面;
当前边界更适合先做 Sewing。
这类提示比直接失败更有用。
把无序边变成 wire
用户选择的边通常是无序的,但构造 TopoDS_Wire 需要有连续的边界关系。
OCCT 提供了 BRepBuilderAPI_MakeWire,可以向里面添加 edge:
BRepBuilderAPI_MakeWire makeWire;
for (const TopoDS_Edge& edge : boundaryEdges) {
makeWire.Add(edge);
}
if (!makeWire.IsDone()) {
// wire 构造失败
}
TopoDS_Wire wire = makeWire.Wire();
这个示例看起来很简单,但真实工程中不能只这样写。
因为 boundaryEdges 的顺序、方向和连接关系都可能不满足要求。如果边界复杂,直接 Add() 可能失败,或者构造出一个不稳定的 wire。
更稳妥的做法是先按端点连接关系排序。
大致思路是:
提取每条 edge 的起点和终点;
根据容差判断端点是否相同;
建立端点到 edge 的连接关系;
从某条 edge 开始追踪下一条;
形成连续 chain;
判断最后是否回到起点;
必要时反转 edge 方向;
输出有序 edge 序列。
这里要特别注意容差。
导入模型里,两条边的端点可能不是完全相等,而是在一个很小距离内接近。如果用严格坐标相等判断,很容易认为边界断开。
但容差也不能太大,否则可能把不该连接的端点连起来。
所以排序 wire 时,容差应该和模型尺度、导入精度、修复上下文有关,而不是写死一个随意的数值。
检查 wire 是否闭合
补面通常要求边界闭合。
如果 wire 不闭合,后续不管是 BRepBuilderAPI_MakeFace 还是 BRepFill_Filling,结果都可能不稳定。
闭合检查不应该只看 wire.Closed()。
在工程里,还需要结合端点距离、边界拓扑和几何连续性做判断。
可以检查:
首尾端点距离是否在容差内;
wire 是否只有一个连续 loop;
是否存在分叉点;
是否存在重复边;
是否存在孤立边;
是否存在自交;
wire 是否包含退化 edge。
其中“分叉点”很容易被忽略。
一个正常的闭合边界里,每个端点通常应该连接两条边。如果某个点连接了三条或更多边,说明用户选择可能包含额外边,或者边界不是单一 loop。
这种情况不适合直接补面。
如果 wire 不闭合,可能有几种处理方式:
提示用户补选缺失边;
尝试用 wire 修复工具连接小 gap;
如果 gap 超过容差,拒绝自动补面;
把问题作为 open boundary 输出报告。
不建议为了补面强行连接距离较大的端点。这种补面结果很可能不是用户想要的。
必要时先修 wire
构造 wire 后,可以考虑使用 ShapeFix_Wire 做一些局部整理。
例如:
Handle(ShapeFix_Wire) wireFixer = new ShapeFix_Wire(wire, face, precision);
wireFixer->SetPrecision(precision);
wireFixer->SetMaxTolerance(maxTolerance);
wireFixer->FixReorder();
wireFixer->FixConnected();
wireFixer->FixClosed();
TopoDS_Wire fixedWire = wireFixer->Wire();
这里的代码只是示意。
实际使用时,要根据是否已有参考 face、是否有 surface、是否只是空间边界来选择更合适的方式。
ShapeFix_Wire 的作用是帮助修复 wire 层面的小问题,例如顺序、连接、闭合等。
但它不应该成为“强行让任意边界可补面”的工具。
如果 wire 本身质量很差,例如边界严重自交、缺边明显、端点距离过大,那么应该停止自动修复,而不是继续往下走。
我更倾向于把 wire 修复作为补面前的保守步骤:
能小范围修正连接关系;
能规整边方向;
能处理轻微 gap;
不能掩盖错误选择;
不能替用户决定大范围闭合。
补面失败时,很多问题其实不是 Filling 或 MakeFace 的问题,而是 wire 本身没有准备好。
判断边界是否共面
如果边界共面,补面相对简单。
可以尝试用 BRepBuilderAPI_MakeFace 根据 wire 构造一个平面面。
例如:
BRepBuilderAPI_MakeFace makeFace(wire);
if (makeFace.IsDone()) {
TopoDS_Face patchFace = makeFace.Face();
}
但这类写法适合比较简单的平面边界。
如果边界不共面,或者边界来自一组曲面边界,直接 MakeFace 可能失败,或者生成的面不符合预期。
工程上可以先判断边界点是否近似共面。
简单做法是:
采样 edge 上的点;
拟合一个参考平面;
计算点到平面的最大距离;
如果距离在容差内,认为近似共面;
否则进入非共面补面流程。
这个判断不需要过度复杂,但一定要有。
因为共面边界和非共面边界适合的补面策略不同。
对于共面边界,目标通常是生成一张平面 face。
对于非共面边界,目标更像是生成一张过渡曲面或填充曲面。
这两种结果对后续网格和仿真也不同。平面补面更容易解释;非共面 Filling 结果则需要更谨慎地验证曲面质量。
非共面边界用 Filling
当边界不共面,或者需要生成一张过渡曲面时,可以考虑 BRepFill_Filling。
它可以根据边界 edge 构造填充面,并支持不同连续性约束。
简化示意如下:
BRepFill_Filling filling;
for (const TopoDS_Edge& edge : orderedEdges) {
filling.Add(edge, GeomAbs_C0);
}
filling.Build();
if (!filling.IsDone()) {
// filling 失败
}
TopoDS_Face patchFace = filling.Face();
这段代码表达的是基本思路,不代表完整工程实现。
实际使用时,还需要考虑:
边界 edge 是否按顺序加入;
是否需要约束到相邻 face;
连续性要求是 C0、G1 还是更高;
边界是否存在小边和尖角;
生成面是否扭曲;
生成面是否和周围面有明显偏差;
是否需要限制曲面阶数或迭代参数。
补面时不要盲目追求高连续性。
在工程修复里,很多时候 C0 连续已经足够,因为目标是闭合拓扑并保证后续网格稳定。
如果强行追求更高连续性,可能导致生成失败或曲面变形更明显。
当然,对于某些建模场景,G1 或更高连续性有意义。
但在 CAD/CAE 修复里,应该先明确目标:是为了美观建模,还是为了修复开口、稳定网格。
生成 face 之后还要 Sewing
这是补面流程里最容易被忽略的一点。
BRepBuilderAPI_MakeFace 或 BRepFill_Filling 成功生成 TopoDS_Face,不代表模型已经修好了。
因为新 face 可能只是一个孤立面。它和原来的周围 face 之间还没有建立共享拓扑关系。
如果直接把它放到 compound 里,显示上可能像补上了,但拓扑检查仍然存在自由边。
所以补面后必须做局部 Sewing。
流程应该是:
生成 patch face;
提取周围邻域 face;
把 patch face 和邻域 face 放入局部集合;
执行 BRepBuilderAPI_Sewing;
获取 Sewing 后的局部 patch;
检查 free edge 是否减少;
检查 shape validity;
再决定是否替换原模型。
也就是说:
MakeFace / Filling 负责生成新面;
Sewing 负责把新面接回周围拓扑;
Analyzer / FreeBounds 负责验证修复效果。
如果跳过 Sewing,补面很容易变成“视觉补面”,而不是“拓扑修复”。
验证补面结果
补面结果至少要验证几类问题。
第一,输出是否有效:
patch face 是否为空;
face 是否有效;
face 是否存在退化边;
face 面积是否异常;
face 是否自交或扭曲严重。
第二,局部修复是否改善:
修复前 free edge 数量;
修复后 free edge 数量;
目标 gap loop 是否消失;
局部 open boundary 是否减少;
是否仍然存在未连接边界。
第三,是否引入新风险:
新 face 是否和周围面错误连接;
局部 shape 是否变成 non-manifold;
是否影响其他不相关面;
是否改变模型整体类型;
是否破坏后续选择或网格。
补面不能只看 API 返回成功。
在工程里,一个失败的补面通常比较容易发现;真正危险的是“看起来成功,但拓扑没有真正变好”。
所以补面结果应该返回结构化信息,而不是只返回一个 TopoDS_Face。
例如:
struct PatchFaceResult
{
bool success = false;
bool valid = false;
bool improved = false;
int freeEdgeCountBefore = 0;
int freeEdgeCountAfter = 0;
TopoDS_Face patchFace;
TopoDS_Shape repairedPatch;
std::string message;
};
这只是示意。重点是:补面结果需要带上判断依据。
替换回原模型
补面最复杂的部分往往不在生成 face,而在替换回原模型。
如果只是生成一个新 face 并展示出来,事情很简单。
但如果要把它变成模型的一部分,就必须处理局部拓扑编辑。
通常需要考虑:
原始 shape 中哪些 face 参与修复;
新生成的 patch face 如何加入;
局部 Sewing 后得到的 shell 如何回写;
未修改区域如何保持不变;
原有 face / edge 引用如何更新;
显示和选择映射如何同步;
撤销时如何恢复原始 shape。
这部分不是 BRepFill_Filling 自动解决的。
它属于 CAD 系统自己的局部拓扑编辑能力。
简单处理可以是重建整个 shape,但这在复杂模型里不一定合适。因为全量重建会影响显示、选择、属性、边界条件、历史记录等一系列对象。
更可控的方式是局部替换:
保存原始局部 patch;
生成修复后的局部 patch;
验证通过后替换对应区域;
更新拓扑索引;
更新显示和选择;
记录撤销信息。
这也是为什么补面功能看起来只是几行 API,但真正工程化要配合文档层、显示层、选择层和撤销系统。
先预览,再提交
补面属于高风险操作,最好不要直接提交。
用户选择边界后,系统可以先生成 patch face 预览。预览阶段不修改原 shape,只显示候选结果。
预览应该让用户看到:
新面的位置;
新面的边界;
参与修复的原始边;
可能被替换的局部区域;
修复前后的 free edge 变化;
是否存在未解决边界。
如果用户确认,再执行局部替换。
这个流程的意义是让用户参与判断。
因为补面结果有时候数学上成立,但不一定符合设计意图。尤其是非共面边界,生成的 filling surface 可能有多种形态,系统不能完全替用户判断哪一种是正确的。
所以交互式补面最好遵循:
先分析;
再预览;
再确认;
再提交;
最后报告。
这种流程比“点击按钮直接补上”要可靠得多。
什么时候放弃
补面功能里,拒绝修复和成功修复一样重要。
遇到下面情况时,应该明确失败,而不是强行生成一个面:
边界不闭合;
存在多个不连通 loop;
边界自交;
端点 gap 超过容差;
edge 方向无法稳定整理;
生成面面积异常;
生成面明显扭曲;
补面后 free edge 没有减少;
修复后 shape validity 变差。
这些情况下,系统应该输出可理解的原因,例如:
当前选择无法组成闭合边界;
当前边界包含多个独立区域;
当前边界不适合自动补面;
建议重新选择边界;
建议先执行 wire 修复;
建议改用局部 Sewing。
这比返回一个失败代码更有用。
用户在交互式修复里并不只是要“成功”,也需要知道为什么失败。只有这样,用户才能调整选择,或者换一种修复方式。
MakeFace 和 Filling 怎么选
从工程角度看,可以大致这样区分:
边界近似共面:优先考虑 MakeFace;
边界不共面:考虑 BRepFill_Filling;
需要过渡曲面:考虑 Filling;
只是已有面没连上:优先 Sewing,不要补面;
wire 本身不稳定:先修 wire。
BRepBuilderAPI_MakeFace 更适合简单、清晰的平面边界。它的结果更容易解释,也更适合保持模型简洁。
BRepFill_Filling 更适合非共面边界或需要填充曲面的场景。但它的结果更依赖边界质量和参数设置,也更需要验证。
这两个工具不是谁更高级的问题,而是适用场景不同。
在 CAD/CAE 修复里,补面目标通常不是做一张很漂亮的自由曲面,而是让局部拓扑更完整,并且不影响后续网格和仿真。
所以选择补面方法时,应该优先考虑稳定性和可解释性。
不要在导入阶段默认补面
补面会新增几何,因此风险比 ShapeFix 和 Sewing 更高。
导入阶段不应该默认补面。
因为导入阶段缺少用户上下文,系统不知道这个开口是否应该闭合,也不知道补出来的面是否符合设计意图。
例如一个开放曲面模型,如果导入时系统自动补面,可能反而破坏了模型。
一个装配体中的间隙,也不应该被自动当成缺面修补。
一个用于仿真的边界开口,可能是用户故意保留的区域。
所以补面更适合放在:
交互式修复命令;
用户确认的局部区域;
专门的几何修复模块;
网格前处理发现问题后的候选修复。
导入阶段最多做检测和提示:
发现 open boundary;
统计 free edge;
记录疑似缺面区域;
提示用户进入修复流程。
这样既能保留原始模型语义,也能让复杂修复有解释和回滚空间。
一个完整流程
综合上面的内容,一个更可控的补面流程可以写成:
1. 用户选择若干 edge
2. 系统判断这些 edge 是否属于同一局部区域
3. 对 edge 做排序,尝试形成连续边界
4. 检查 wire 是否闭合
5. 必要时做保守 wire 修复
6. 判断边界是否近似共面
7. 共面时尝试 MakeFace
8. 非共面时尝试 Filling
9. 生成 patch face
10. 将 patch face 和邻域 face 做局部 Sewing
11. 检查 free edge 是否减少
12. 检查 shape validity
13. 生成预览
14. 用户确认后替换局部 patch
15. 更新显示、选择和撤销信息
16. 输出修复报告
这看起来比一个 fillHole() 复杂很多。
但只有这样,补面才更接近工程可用,而不是一次不可解释的 API 尝试。
小结
从几条边创建修补面,真正困难的不是“生成面”,而是围绕生成面的前后流程。
我现在更认可的判断是:
用户选择的边只是线索,不是直接可用的补面边界;
补面前必须构造稳定 wire;
wire 是否闭合比 API 调用更重要;
共面边界和非共面边界应该走不同策略;
Filling 不是万能补面;
补面后必须 Sewing;
补面结果必须验证 free edge 和 shape validity;
局部替换、显示更新和撤销属于补面功能的一部分;
高风险补面应该走预览和用户确认。
如果没有这些工程步骤,补面功能很容易变成“某些简单模型能用,复杂模型不稳定”的工具。
OCCT 提供了很有用的补面能力。
BRepBuilderAPI_MakeWire 可以帮助构造 wire,BRepBuilderAPI_MakeFace 可以从简单边界生成面,BRepFill_Filling 可以处理更复杂的填充面,BRepBuilderAPI_Sewing 可以把新面接回周围拓扑。
但这些 API 只是执行器。
真正决定补面是否可靠的,是前面的边界分析和后面的验证回写。
从工程角度看,补面应该被理解成:
选择边界;
分析边界;
构造 wire;
生成 patch face;
局部 Sewing;
验证结果;
替换回模型;
支持预览、报告和撤销。
只有这样,从几条边创建修补面才不是一个演示功能,而是可以放进 CAD/CAE 修复流程里的工程能力。