Technical note

CAD 面缝隙修复

记录一次 CAD/CAE 面缝隙修复的工程思路:free edge 只是现象,不等于缺面。真正的修复需要先区分拓扑未连接、真实缺面、开放边界和近接触风险,再选择 Sewing、Wire 修复、局部补面或放弃自动处理。


背景

在 CAD 几何修复里,free edge 是一个非常常见的问题入口。

一个 shell 本来应该闭合,但检查后发现存在自由边;或者导入后的模型看起来像一个实体,但后续做布尔、网格或体域识别时发现它并不是闭合体。这时候最直观的想法通常是:既然有开口,那是不是补一张面就好了?

实际工程里,这个判断很容易出错。

free edge 只是现象,不是问题本身。它只能说明某条 edge 在拓扑上没有被两个 face 共同使用,但不能直接告诉我们这个位置到底发生了什么。

同样是一圈自由边,背后可能有完全不同的原因:

两个相邻面几何上贴近,但拓扑上没有连接;
局部确实缺失了一张面;
导入容差导致边界没有合并;
wire 本身存在断裂或方向问题;
模型本来就是开放曲面;
开口是用户有意保留的结构边界;
不同零件之间靠得很近,但不应该被缝成一个整体。

所以面缝隙修复的第一步,不应该是补面,而应该是判断:

这个 free edge 到底意味着什么。

这篇文章记录的就是这个判断过程:为什么 free edge 不等于缺面,以及工程上应该如何组织面缝隙修复流程。

Free edge 是信号

在 BRep 模型里,edge 会被 face 引用。

如果某条 edge 只被一个 face 引用,它就是自由边。对于一个完整封闭的 shell 来说,内部边界通常应该被相邻两个 face 共同使用。

因此,free edge 通常意味着模型局部存在开放边界。

但它本身并不说明应该怎么修。

举一个简单例子:两个面在空间上贴得很近,各自有一条边,几何位置几乎重合,但它们不是同一条拓扑边。这个时候统计出来会有两条 free edge。

如果直接补面,方向可能完全错了。真正应该做的可能只是 Sewing,把两个已有面缝起来。

再看另一种情况:几个面围成一个洞,中间确实缺少一张面。这时候 Sewing 也不够,因为没有可缝合的目标面。需要先根据边界构造修补面,再把新面和周围面 Sewing。

所以 free edge 更适合作为检测结果,而不是修复结论。

比较稳妥的流程是:

检测 free edge;
把 free edge 聚成边界;
判断边界是否闭合;
分析相关 face 的邻域关系;
区分拓扑未连接、真实缺面、开放边界和近接触;
再选择修复策略。

这一步做不好,后面的 Sewing、Filling、Wire 修复或 Extend / Intersect 都可能用错。

情况一:只是没缝上

最适合 Sewing 的情况,是已有面之间几何上可以连接,但拓扑上没有连接。

这种问题在导入模型里很常见。例如 IGES 或一些面片化数据导入后,模型看起来像一个完整外壳,但内部是许多独立 face。它们在几何上相邻,但 edge 并不共享。

这种情况下,问题不是缺面,而是拓扑关系没有建立。

典型特征是:

相邻 face 都存在;
边界几何距离很小;
free edge 往往成对出现;
补面没有必要;
局部 Sewing 后 free edge 数量可能明显下降。

这类问题应该优先尝试 BRepBuilderAPI_Sewing

工程流程可以是:

提取问题区域附近的 face;
统计修复前 free edge;
使用保守容差做局部 Sewing;
统计修复后 free edge;
检查 shape validity;
如果问题减少,则接受结果;
如果没有改善,不要继续盲目增大容差。

这里要强调“局部”。

直接对整个模型做全局 Sewing 虽然简单,但风险更高。特别是装配体或存在近接触面的模型,过大的输入范围可能会把不该连接的边界缝在一起。

所以在面缝隙修复里,Sewing 应该优先作为局部修复策略,而不是默认全局强修复。

情况二:确实缺面

另一类情况是真正缺面。

例如若干自由边形成一个闭合边界,中间没有对应 face。此时模型不是“面之间没连上”,而是局部几何缺了一块。

这种情况下,只做 Sewing 通常没有用。因为 Sewing 连接的是已有面,它不会凭空生成缺失面。

这类问题需要局部补面。一般流程是:

从 free edge 中提取边界;
判断边界是否能组成闭合 wire;
修复 wire 的连接和方向问题;
根据 wire 构造新 face;
把新 face 和周围面 Sewing;
检查 free edge 是否减少;
验证修复后的 shape 是否有效。

如果边界是平面的,可以尝试用 BRepBuilderAPI_MakeFace 从 wire 构造面。

如果边界不共面,或者需要生成一张过渡曲面,可以考虑使用 BRepFill_Filling

示意流程大概是:

TopoDS_Wire boundaryWire = ...;

BRepFill_Filling filling;
for (const TopoDS_Edge& edge : boundaryEdges) {
    filling.Add(edge, GeomAbs_C0);
}

filling.Build();

if (filling.IsDone()) {
    TopoDS_Face patchFace = filling.Face();
    // 再把 patchFace 与周围 face 做局部 Sewing
}

这段代码只是表达思路。真正工程里不能直接把几条边扔进去就结束,还要处理边界排序、闭合判断、方向一致性、边界质量和结果验证。

补面比 Sewing 风险更高。

因为 Sewing 更多是在已有拓扑之间建立连接,而补面是在模型里新增几何。新增一张 face 可能改变 shell 闭合状态、体域识别和后续网格结果。

所以补面不适合在导入阶段静默执行,更适合放在交互式修复或专门修复命令里。

情况三:Wire 本身有问题

有些 free edge 不是因为缺面,而是因为 wire 自身质量不好。

例如:

边没有首尾相接;
边之间存在很小 gap;
wire 方向混乱;
边界存在重复边;
局部存在非常短的小边;
wire 自交。

这种情况下,直接 Sewing 或 Filling 都可能失败,因为后续修复依赖的边界本身就不可靠。

这时候应该先做 wire 层面的整理。

OCCT 里可以考虑使用 ShapeFix_Wire 或相关 wireframe 修复工具,对局部 wire 做连接、闭合和小缺陷处理。

简化思路是:

收集边界 edge;
尝试排序成 wire;
检查 wire 是否闭合;
修复连接关系;
处理小 gap;
必要时过滤明显退化边;
再进入 Sewing 或 Filling。

这里也要谨慎。

比如删除小边这件事,在修复角度可能是合理的;但从模型语义看,小边可能代表真实特征。对于 CAD/CAE 模型,小倒角、小台阶、小缝隙不一定都是噪声。

所以 wire 修复最好只在明确的局部区域内进行,并且要记录修复前后的边界变化。

情况四:本来就是开放曲面

不是所有 free edge 都应该被修掉。

有些模型本来就是开放曲面。例如薄片、曲面片、开放壳、导入的参考面,或者用户故意保留的边界。

这些情况下,free edge 是合法边界,不是错误。

如果修复模块看到 free edge 就尝试闭合,反而会破坏用户意图。

这类问题在工程里很容易被误判。因为从几何检查角度看,它确实存在开放边界;但从业务语义看,它可能完全正常。

所以修复模块应该先判断:

这个 shape 是否期望成为 solid;
当前命令是否要求闭合 shell;
用户是否选择了开口区域进行修复;
后续流程是否需要体网格;
是否只是显示或参考几何。

如果没有明确的闭合目标,不应该自动补面。

这也是为什么几何修复需要上下文。

同一个 free edge,在导入阶段可能只是记录问题;在体网格前处理阶段可能需要提醒用户;在交互式修复命令里才适合尝试补面。

情况五:Near contact 不是缝隙

CAD/CAE 模型里还有一类比较麻烦的问题:near contact。

两个面之间距离非常近,看起来像缝隙,也可能影响后续网格,但它们未必应该被 Sewing 或补面。

例如两个零件之间存在很小间隙。从几何上看,这个间隙可能会导致网格很难处理;但从模型语义上看,它们可能是两个独立零件,不应该被缝成一个整体。

这类问题不能简单归类为 free edge 修复。

它更接近共形网格风险或接触关系消歧问题。处理方式可能是面贴合、微小偏移、局部布尔、接触关系标记,或者只做风险提示。

这类策略通常比 Sewing 和 Filling 更敏感,不适合在基础文章里展开完整算法。但可以明确一点:

近接触问题不应该默认当成面缝隙修复;
能 Sewing 的是已有面边界连接问题;
能 Filling 的是真正缺面问题;
near contact 需要结合后续网格和装配语义单独判断。

这个边界很重要。否则面缝隙修复很容易越界,变成破坏模型语义的强修复。

先做 Region

如果模型里有大量 free edge,逐条边处理通常不可行。

单条 free edge 的信息太少。真正有意义的是一组边界形成的局部区域。

所以更合理的做法是把 free edge 聚成 region。

一个 region 可以表示:

一段开放边界;
一圈可能的孔洞;
一组相关 gap;
一个局部 open shell 区域;
一个需要用户确认的问题区域。

这样后续修复策略才有上下文。

例如同样是十条 free edge,如果它们形成一个闭合 loop,就可能适合补面;如果它们是两组相互靠近的边界,就可能适合 Sewing;如果它们分布在开放曲面外边界上,可能不应该修。

Region 级别的判断通常比 edge 级别更可靠。

大致流程可以是:

统计 free edge;
按端点连接关系聚类;
形成 gap chain 或 gap loop;
计算 region 的边界特征;
关联周围 face;
判断 region 类型;
选择修复策略。

这里不需要一开始就做得很复杂,但至少要避免“看到一条 free edge 就补一张面”的处理方式。

局部修复更可控

面缝隙修复应该尽量局部化。

全局修复最大的问题是影响范围不可控。例如对整个 shape 做 Sewing 或 ShapeFix,看起来省事,但修复后很难判断到底哪些区域发生了变化。

局部修复的思路是:

先定位问题区域;
提取相关 face 和 edge;
构造局部 patch;
在 patch 内尝试修复;
验证修复效果;
再把局部结果放回原模型。

这样做的好处是:

修复范围明确;
失败后容易回滚;
用户能看到问题区域;
修复报告更清楚;
不容易误改模型其他部分。

当然,局部修复也有自己的难点。

比如局部结果怎么替换回原 shape,周围拓扑怎么保持,原来的 face / edge 索引关系怎么更新,修复后选择和显示怎么同步。

这些都不是 BRepFill_FillingBRepBuilderAPI_Sewing 自动解决的。它们属于工程系统中的局部拓扑编辑能力。

这也是为什么面缝隙修复不能只写成一个补面函数。真正的工作往往在补面之前和补面之后。

策略要有优先级

面缝隙修复里,策略选择很重要。

我更倾向于按风险从低到高尝试,而不是一上来就补面。

可以粗略理解为:

先检测,不修改;
能确认是拓扑未连接的,优先局部 Sewing;
wire 自身有问题的,先做 wire 修复;
确实缺面的,再尝试局部补面;
补面后再 Sewing;
修复后验证;
高风险或不确定问题交给用户确认。

这个顺序的核心是:尽量先做低风险修复,再做会新增几何或改变结构的修复。

Sewing 通常比补面风险低。

Wire 整理通常比新增 face 风险低。

局部修复通常比全局修复风险低。

用户确认通常比静默强修复更安全。

当然,这不是固定规则。不同业务场景会有不同策略。但工程上一定要有优先级意识,不能把所有问题都丢给一个万能修复流程。

补面之后还要 Sewing

补面不是终点。

很多时候,BRepFill_FillingBRepBuilderAPI_MakeFace 成功生成 face 后,修复流程还没有完成。

因为新面可能只是一个独立 face,还没有和周围原有面共享拓扑边。

这时必须把新面和周围面一起做局部 Sewing。

流程可以是:

生成 patch face;
把 patch face 和邻域 face 组成局部集合;
执行 Sewing;
检查新旧面是否连接;
统计 free edge 是否减少;
判断 shell 是否更接近闭合。

如果补面后不做 Sewing,可能会出现一种假成功:代码生成了一张面,显示上也像补上了,但拓扑检查仍然存在自由边,后续网格仍然失败。

所以补面流程至少要包含三步:

生成面;
连接面;
验证面。

只做第一步是不够的。

修复后要验证

无论 Sewing 还是 Filling,API 成功都不代表修复成功。

面缝隙修复至少要检查:

输出 shape 是否为空;
shape 基础有效性是否通过;
free edge 数量是否下降;
gap region 是否减少;
补面是否引入新的自由边;
局部 patch 是否仍然开放;
shape 类型是否发生非预期变化;
后续网格或布尔流程是否更稳定。

尤其是 free edge 数量,最好做修复前后对比。

例如:

修复前 free edge = 32;
局部 Sewing 后 free edge = 12;
补面后 free edge = 4;
最终仍有 4 条自由边,需要用户确认。

这种结果虽然不是完全成功,但它是可解释的。比单纯返回 truefalse 更有价值。

一个修复报告应该让用户知道:

系统发现了什么问题;
尝试了哪种策略;
修复了哪些区域;
还有哪些问题没有解决;
是否建议继续修复。

这也是交互式修复里非常重要的一部分。

不要在导入时强修

导入阶段经常能发现 free edge 和 open boundary,但这不意味着导入流程应该自动修掉它们。

原因很简单:导入阶段缺少上下文。

系统不知道这个模型是否应该闭合,不知道这个开口是不是设计意图,不知道用户后续是显示、建模、网格还是仿真,也不知道某些小缝隙是否代表真实结构。

如果导入时默认强修复,可能会带来几个问题:

补掉本来应该保留的开口;
缝合不该缝合的零件;
删除真实小特征;
改变后续边界条件选择;
让用户不知道模型已经被修改;
后续出问题时很难回溯。

所以导入阶段更适合做轻量处理:

基础 ShapeFix;
基础有效性检查;
free edge 统计;
问题区域记录;
必要时给用户提示。

复杂的面缝隙修复应该进入专门修复模块或交互式命令,由用户确认局部区域后再执行。

交互式修复更合适

面缝隙修复非常适合做成交互式工具。

因为很多时候,系统可以检测问题,但无法完全判断用户意图。用户选中几条边或几张面后,修复范围和目标会清楚很多。

一个比较自然的交互流程是:

用户选择问题边界或面;
系统识别 free edge / gap region;
显示局部问题预览;
系统给出 Sewing、补面或跳过建议;
用户确认修复;
执行局部 patch;
显示修复前后差异;
输出修复报告。

这种方式比完全自动修复更符合 CAD 软件的交互逻辑。

它不是让用户手工处理所有细节,而是让系统提供候选修复,同时保留用户确认权。

对于高风险操作,比如补面、扩展面、近接触贴合,这种交互式确认尤其重要。

结果应该包含什么

面缝隙修复不适合只返回一个 TopoDS_Shape

更合理的结果应该包含:

是否成功;
是否有改进;
修复策略;
修复前 free edge 数量;
修复后 free edge 数量;
生成的新 face 数量;
是否执行了 Sewing;
是否仍存在 open boundary;
是否需要用户进一步处理;
修复后的 shape。

示意结构可以写成:

struct GapRepairResult
{
    bool success = false;
    bool improved = false;
    bool valid = false;

    int freeEdgeCountBefore = 0;
    int freeEdgeCountAfter = 0;
    int createdFaceCount = 0;

    TopoDS_Shape resultShape;
    std::string message;
};

这只是表达思路,不是固定接口。

重点是:修复结果要包含判断依据。上层不能只知道“修复函数返回了一个 shape”,还要知道这个 shape 为什么可以接受。

什么时候放弃

面缝隙修复不是所有情况都应该自动完成。

下面这些情况就要谨慎:

边界无法形成稳定 wire;
free edge 不构成闭合 loop;
边界自交严重;
补面质量无法保证;
修复后 free edge 没有减少;
shape validity 变差;
问题区域跨越多个零件;
用户意图不明确;
可能影响后续边界条件或材料区域。

这时候应该停止自动修复,输出问题报告,或者让用户重新选择局部区域。

自动修复最怕的是“看起来修好了”。显示上补了一张面,但拓扑仍然坏;或者拓扑闭合了,但模型语义被改了。

这类问题比修复失败更危险。

所以在不确定时,保守一点通常更好。

小结

面缝隙修复真正要解决的,不是“怎么补一张面”,而是“如何判断这个位置到底该不该补面”。

我现在更认可的判断是:

free edge 是检测信号,不是修复策略;
Sewing 适合处理已有面之间的拓扑未连接;
Filling 适合处理确认缺面的局部补面;
wire 修复是 Sewing 和 Filling 的前置准备;
open surface 不应该被默认强行闭合;
near contact 不应该被简单当成面缝隙;
局部修复比全局修复更可控;
修复结果必须有验证和报告。

这也是面缝隙修复比看起来复杂的原因。

它涉及检测、分类、策略选择、局部拓扑编辑、OCCT API 调用和结果验证,不是一个 fillHole() 函数能解决的。

CAD 面缝隙修复里,最容易犯的错是看到 free edge 就补面。

free edge 只是一个拓扑信号。它可能代表拓扑没连上,也可能代表真实缺面,也可能是开放曲面的正常边界,还可能是近接触或装配关系带来的风险。

所以更稳妥的修复流程应该是:

先检测 free edge;
再聚成 region;
再判断问题类型;
能 Sewing 的先 Sewing;
确实缺面的再补面;
补面后再 Sewing;
修复后做 free edge 和 shape validity 检查;
高风险问题交给用户确认。

从这个角度看,面缝隙修复不是单个 OCCT API 的调用,而是一套工程策略。

OCCT 提供了 ShapeFix_WireBRepBuilderAPI_SewingBRepFill_Filling 等基础能力,但真正决定修复质量的,是前面的分类和后面的验证。

只有把 free edge 当成线索,而不是结论,面缝隙修复才会更可控。