Technical note

OCCT 常用模型修复接口梳理:ShapeFix、Sewing 与 Filling

整理 CAD/CAE 几何修复中常见的 OCCT 接口,包括 BRepCheck、ShapeFix、BRepBuilderAPI_Sewing、ShapeAnalysis_FreeBounds 和 BRepFill_Filling,并记录它们在工程使用中的边界。


背景:模型修复不能只靠一个 API

做 CAD/CAE 几何修复时,很容易有一个误区:看到模型导入后存在自由边、开口壳、坏 wire 或局部面缝隙,就希望找到一个“万能修复接口”,把 shape 丢进去,然后得到一个干净、闭合、可网格化的模型。

实际工程里基本不会这么简单。

OCCT 确实提供了不少修复相关工具,比如 BRepCheck_AnalyzerShapeFix_ShapeShapeFix_FaceShapeFix_WireBRepBuilderAPI_SewingShapeAnalysis_FreeBoundsBRepFill_Filling 等。

但这些接口解决的问题层次并不一样:

  • 有的适合做基础有效性检查;
  • 有的适合做轻量修复;
  • 有的适合修 face 或 wire;
  • 有的适合把已有面缝起来;
  • 有的适合根据边界补一张新面。

如果把它们都简单归类为“修复 API”,代码很容易变成一串黑盒调用:

checkShape();
fixShape();
sewShape();
fillShape();
checkAgain();

流程看起来完整,但几个关键问题其实没有回答:

每一步到底修了什么?
有没有改变原始拓扑语义?
失败后要不要回滚?
修复结果能不能进入后续网格流程?
用户怎么知道哪里被修过?

所以这篇文章不想写成 API 手册,而是从工程使用角度梳理一下:这些 OCCT 修复接口分别适合解决什么问题,以及它们的边界在哪里。

BRepCheck_Analyzer:先判断问题是否真的存在

在真正修复之前,我一般会先做检查,而不是直接修。

OCCT 里常见的基础检查工具是 BRepCheck_Analyzer。它可以用来判断一个 TopoDS_Shape 在 OCCT 的基础规则下是否有效。

典型用法大致是:

BRepCheck_Analyzer analyzer(shape);
if (!analyzer.IsValid()) {
    // shape 存在基础有效性问题
}

这一步看起来很普通,但在工程里很重要。因为很多修复流程的第一个问题不是“怎么修”,而是“这个问题是否真的存在”。

有些模型并不是一个理想 solid,但对当前流程可能已经够用。例如只用于显示、测量或局部选择时,它不一定需要被强制修成闭合实体。

反过来,也有一些模型能通过 BRepCheck_Analyzer,但后续在布尔、网格、共形处理或接触识别时仍然会暴露问题。

所以我更倾向于把 BRepCheck_Analyzer 理解成基础体检工具,而不是完整质量评估工具。

它适合回答的问题是:

这个 shape 在 OCCT 基础规则下是否有效;
某些 face、wire、edge 是否存在明显拓扑问题;
修复前后,基础有效性有没有变差。

它不适合回答的问题是:

这个模型是否适合 CAE 网格;
两个面之间是否存在近接触风险;
局部小缝隙是否会影响后续共形;
模型语义是否被修复流程改变。

这也是后来做几何修复模块时,我更倾向于把“检查”单独作为一层,而不是在每个修复函数里随手塞一个 IsValid() 就结束。

ShapeFix_Shape:适合作为轻量修复入口

ShapeFix_Shape 是很多人接触 OCCT 修复时最先用到的接口。它的优势是入口统一,可以对一个 shape 做一轮基础修复。

典型流程大概是:

Handle(ShapeFix_Shape) fixer = new ShapeFix_Shape(shape);
fixer->SetPrecision(precision);
fixer->SetMaxTolerance(maxTolerance);
fixer->Perform();

TopoDS_Shape fixedShape = fixer->Shape();

在工程里,ShapeFix_Shape 比较适合处理一些轻量、常规、风险较低的问题,例如:

wire 连接关系整理;
部分 face / wire 的基础修复;
容差范围内的小问题规整;
导入后 shape 的基础预处理。

但它不应该承担“复杂模型修复”的全部职责。

原因很简单:ShapeFix_Shape 不理解你的业务目标。

同一个模型,在不同场景下修复目标可能完全不同:

只是显示出来;
参与布尔运算;
生成表面网格;
生成体网格;
保持接触面共形;
保持原始导入语义。

这些目标不同,能接受的修复程度也不同。

有些场景希望尽量闭合模型,有些场景更在意保持原始拓扑;有些场景可以接受删除细小边,有些场景里小边可能就是边界条件或真实结构特征。

所以我现在更倾向于把 ShapeFix_Shape 看成一个“基础清理工具”,而不是最终修复策略。

比较稳妥的使用方式是:

轻量修复可以用 ShapeFix_Shape;
复杂修复不要只依赖 ShapeFix_Shape;
修复前后必须有检查;
失败时要能保留原始 shape;
不要在用户不知情的情况下做高风险拓扑修改。

这也是为什么我不建议在模型导入阶段默认启用太激进的修复流程。导入阶段可以做保守处理,但不应该替用户决定哪些几何特征应该被修改或删除。

ShapeFix_Face:面级修复适合局部处理

如果问题集中在 face 层面,ShapeFix_Face 会比直接对整个 shape 做全局修复更可控。

例如某个 face 的边界 wire 存在连接问题、闭合问题或局部自交问题,直接全局修复可能影响整个 shape;而面级修复可以把影响范围限制在一个更小的局部区域内。

简化后的流程类似:

TopoDS_Face face = ...;

Handle(ShapeFix_Face) faceFixer = new ShapeFix_Face(face);
faceFixer->SetPrecision(precision);
faceFixer->SetMaxTolerance(maxTolerance);

Handle(ShapeFix_Wire) wireTool = faceFixer->FixWireTool();
wireTool->FixClosed(Standard_True);
wireTool->FixConnected(Standard_True);

faceFixer->Perform();

TopoDS_Face fixedFace = faceFixer->Face();

这里有一个工程判断:面级修复不是为了“把所有面都过一遍”,而是为了处理已经定位到的局部问题。

如果一个 shape 有几千、几万个 face,直接逐面修复不一定划算。很多 face 本来没有问题,强行处理反而会带来额外成本和不确定性。

更合理的方式是:

先检测问题;
定位到可疑 face 或边界;
对局部 face 做修复;
再把结果放回局部区域;
最后做整体或局部验证。

ShapeFix_Face 本身只是工具,真正难的是决定:

什么时候用它?
对哪一块用?
修完怎么验收?
修失败后要不要回滚?

这些判断不应该散落在每个修复函数里,而应该进入更上层的策略层。

ShapeFix_Wire:wire 修复通常是补面和缝合前的准备

很多几何修复问题最后都会落到 wire 上。

例如:

面边界没有闭合;
几条边首尾没有连好;
边方向不一致;
边界上有很短的小边;
wire 存在局部自交。

这些问题如果不先处理,后面的 sewing、filling、make face 都可能失败。

ShapeFix_Wire 适合处理 wire 层面的连接和规整问题。它通常不会单独成为最终修复结果,而是作为后续操作的准备步骤。

例如,从几条边构造一个修补面之前,一般不能直接把边丢给 BRepFill_FillingBRepBuilderAPI_MakeFace。更稳妥的做法是先判断这些边能否形成合理边界,再构造 wire,并在必要时做 wire 级修复。

一个典型思路是:

收集边;
判断边是否首尾可连接;
构造 wire;
修复 wire 的连接和闭合问题;
再尝试 make face 或 filling;
最后验证生成面是否有效。

在 CAD/CAE 工程里,wire 问题经常不是最终问题,而是更上层问题的表现。

比如一个 open shell 的自由边,表面看是 free edge,但本质上可能是:

两个面没有拓扑连接;
局部确实缺了一块面;
导入容差导致边界没有合并;
模型本来就是开放曲面;
该位置是用户故意保留的开口。

所以看到 wire 问题时,不要急着直接修 wire。先判断它属于哪一类问题,通常更重要。

ShapeFix_Wireframe:处理线框缺陷时要注意副作用

ShapeFix_Wireframe 常用来处理更偏线框层面的缺陷,例如小边、wire gap 等。

示意用法大致是:

Handle(ShapeFix_Wireframe) wireframeFixer = new ShapeFix_Wireframe(shape);
wireframeFixer->SetPrecision(precision);
wireframeFixer->SetMaxTolerance(maxTolerance);

wireframeFixer->ModeDropSmallEdges() = Standard_True;
wireframeFixer->FixSmallEdges();
wireframeFixer->FixWireGaps();

TopoDS_Shape fixedShape = wireframeFixer->Shape();

这类工具在工程里很有用,但也要谨慎。

例如删除小边这件事,从修复角度看可能是合理的;但从建模语义看,小边可能代表真实结构特征。对于 CAD/CAE 模型来说,小倒角、小缝、小台阶有时候不是噪声,而是物理结构的一部分。

因此线框修复不能只看“模型更干净了没有”,还要看:

是否误删了真实特征;
是否影响后续边界条件选择;
是否改变了用户能看到和能选中的拓扑;
是否影响后续网格尺寸和局部加密。

这也是为什么我不太赞成在导入阶段默认启用太激进的线框修复。

线框修复可以做,但它应该出现在可控的修复流程里,而不是在导入时静默执行。

BRepBuilderAPI_Sewing:连接已有面,不是自动补面

BRepBuilderAPI_Sewing 是几何修复里非常常用的接口。

它的作用可以简单理解为:在给定容差内,把相邻面之间能够连接的边界缝合起来。

简化用法是:

BRepBuilderAPI_Sewing sewing(tolerance);
sewing.Add(shape);
sewing.Perform();

TopoDS_Shape sewedShape = sewing.SewedShape();

它适合处理的问题包括:

多个 face 拓扑上没有连接;
shell 存在可缝合的开口;
相邻面边界几何上接近,但拓扑上没有共享;
导入后 face 集合需要组成 shell。

但这里有一个很重要的边界:

Sewing 是连接已有面,不是凭空补一张面。

如果模型缺了一块面,只剩周围一圈自由边,BRepBuilderAPI_Sewing 不会自动理解并生成缺失面。它可能让已有面之间连接得更好,但它不是补面算法。

所以在工程里,Sewing 适合作为这些流程的一部分:

导入后 face 集合的轻量拓扑连接;
局部 patch 修复后的重新缝合;
补面之后,把新面和周围旧面连接起来;
面级修复之后,重新固化局部拓扑关系。

Sewing 最大的问题通常来自容差。

容差太小,原本应该连接的面缝不上;容差太大,不该连接的边界可能被错误合并。这个问题在 CAD/CAE 里尤其敏感,因为错误缝合可能会改变后续网格拓扑,甚至影响边界条件、接触关系和材料区域。

所以我现在更倾向于把 Sewing 当成一个需要上下文的操作:

它修的是哪些面?
输入是不是局部 patch?
容差怎么来?
修复前自由边有多少?
修复后自由边是否下降?
shape validity 有没有变差?
失败时是否需要回滚?

只有这些信息都在,Sewing 才是一个工程可控的修复步骤。

ShapeAnalysis_FreeBounds:自由边是很多修复流程的入口

在处理 open shell、面缝隙、补面问题时,自由边通常是最重要的线索之一。

自由边可以理解为:某条 edge 只被一个 face 使用,说明它处在边界上。如果一个 shell 本来应该闭合,却存在大量自由边,就说明局部拓扑没有闭起来。

OCCT 中可以通过拓扑映射自己统计自由边,也可以结合 ShapeAnalysis_FreeBounds 把自由边连接成 wire。

一个常见流程是:

先统计 edge 被多少个 face 使用;
找出只属于一个 face 的 edge;
把这些 free edge 尝试连接成 wire;
判断 wire 是否能形成闭合边界;
再决定是 sewing、补面,还是放弃自动处理。

ShapeAnalysis_FreeBounds 比较适合做自由边整理和边界识别。它不是修复接口本身,但它经常是修复流程的前置步骤。

这里也有一个工程上的注意点:

free edge 只是现象,不是结论。

一个 free edge 可能意味着:

两个面只是拓扑没连上;
局部确实缺了一张面;
导入容差导致边界没匹配;
模型本来就是开放曲面;
该位置是用户故意保留的开口。

如果不区分这些情况,直接对 free edge 做补面或 sewing,很容易修错。

所以更稳妥的做法是:先把 free edge 聚成局部边界,再结合所在 face、邻域、模型类型和后续用途做判断。

BRepFill_Filling:从边界构造修补面

当模型确实存在缺面时,Sewing 通常不够用。这时需要考虑补面。

OCCT 中常见的补面思路之一是使用 BRepFill_Filling,根据边界约束构造一张填充面。它适合处理一些局部孔洞或非严格平面的边界补面问题。

从工程流程看,补面一般不是一个单独 API 调用,而是一串步骤:

找到问题边界;
判断边界是否可以组成 wire;
检查边界是否闭合;
根据边界生成填充曲面;
把曲面转成 face;
将新 face 与周围面 sewing;
验证自由边是否减少;
验证 shape 是否仍然有效。

如果边界比较简单、共面性较好,有时也可以用 BRepBuilderAPI_MakeFace 直接从 wire 创建面。

但对于非共面边界,或者边界需要生成一张过渡曲面时,BRepFill_Filling 会更合适。

不过补面比 Sewing 风险更高。

原因是 Sewing 更多是在已有拓扑之间建立连接,而 Filling 是在模型中新增几何。新增一张面之后,必须考虑它是否符合原始设计意图。

尤其在 CAE 模型里,补出来的面可能会改变封闭区域、体域识别、网格拓扑和边界条件。

所以补面不适合在导入阶段默认执行。它更适合放在交互式修复或专门修复命令里,让用户能看到问题位置、确认修复范围,并在修复后检查结果。

补面流程中最容易出问题的地方包括:

边界没有真正闭合;
边方向不一致;
边界自交;
边界不共面;
生成面质量不好;
新面和旧面 sewing 后仍然存在自由边;
补面导致 shape 类型或拓扑结构发生非预期变化。

所以补面一定要有验收逻辑,不能只看 API 是否返回成功。

为什么这些接口需要再包一层工程策略

如果只看 OCCT API,本身已经有检查、修复、缝合、补面工具。那为什么工程里还需要再包一层?

核心原因是:

OCCT 提供的是工具,不是业务决策。

例如同样是 free edge,可能有几种处理方式:

如果只是拓扑没连接,可以尝试 sewing;
如果是 wire gap,可以尝试 wireframe fix;
如果确实缺面,可以尝试 filling;
如果是开放曲面,不应该强行闭合;
如果会影响后续共形网格,需要更谨慎地处理接触关系。

这些判断不是某一个 API 能自动完成的。

工程修复模块真正要解决的是:

问题检测;
问题分类;
策略选择;
修复执行;
结果验证;
失败回滚;
修复报告。

这也是后来我更倾向于把修复流程拆成几层:

检查层:判断 shape 是否存在问题;
分析层:把问题归类为 free edge、face gap、open boundary 等;
策略层:决定使用 ShapeFix、Sewing、Filling 还是局部编辑;
执行层:调用 OCCT 接口或自定义局部修复;
验证层:检查修复前后变化;
报告层:告诉用户修了什么、有没有风险。

这样做的好处是,代码不会变成一串黑盒 API 调用。后续如果某个模型修复失败,也更容易判断问题出在哪里:是检测不准、策略选错、容差不合适,还是某个 API 本身解决不了这个问题。

导入阶段应该轻一点

模型导入阶段和专门修复阶段应该分开。

导入阶段的目标是稳定读入模型,尽量保留原始几何和拓扑语义。它可以做一些轻量、保守、可预期的处理,比如基础检查、单位处理、简单 shape fix、必要的拓扑整理。

但导入阶段不应该默认做强修复。

原因有三个。

第一,强修复可能改变模型语义。比如删除小边、合并边界、补面、扩大容差 sewing,这些操作都可能让模型变得“更干净”,但不一定更正确。

第二,导入阶段缺少用户上下文。系统不知道用户接下来是要显示、建模、布尔、网格,还是设置边界条件。不同用途对修复的接受程度不同。

第三,复杂修复需要解释和回滚。用户应该知道哪里被修了、用了什么策略、修复前后自由边是否减少、shape 是否仍然有效。如果这些都藏在导入流程里,后续出问题很难排查。

所以更稳妥的边界是:

导入阶段:做轻量修复和问题记录;
修复模块:做检测、分类、策略选择和可解释修复;
交互命令:处理局部补面、面缝合、近接触等高风险操作。

这种边界看起来麻烦,但对于 CAD/CAE 系统来说更可控。

修复结果不能只看是否生成成功

几何修复里还有一个常见误区:只要 API 返回成功,就认为修好了。

实际上并不是。

一个修复结果至少要看几类指标:

shape 是否为空;
BRepCheck_Analyzer 是否通过;
自由边数量是否下降;
问题区域是否减少;
shape 类型是否发生非预期变化;
修复后的拓扑是否还能被后续流程识别;
是否影响用户可见的边、面和选择对象。

尤其是 free edge 和 face gap 修复,不能只看生成了一个新的 shape。因为新的 shape 可能确实生成成功,但自由边没有减少,甚至引入了更多边界问题。

补面也是一样。生成一张 face 不代表孔洞被正确修复。还要看这张 face 是否和周围面缝上了,边界是否还开放,局部拓扑是否有效。

所以工程上应该把修复结果写成报告,而不是只返回一个 bool。

一个更有用的修复结果应该包含:

是否成功;
使用了什么策略;
修复前问题数量;
修复后问题数量;
是否有回滚;
是否存在仍未解决的问题;
是否需要用户进一步确认。

这类信息对于交互式修复尤其重要。用户不只是想知道“修好了没有”,还想知道“修了哪里,怎么修的,会不会影响模型”。

小结:OCCT 提供工具,但工程修复需要边界

OCCT 的修复接口很强,但它们更像一组工具箱,而不是完整的修复系统。

对于 CAD/CAE 场景来说,模型修复真正困难的地方往往不在于调用 ShapeFixSewingFilling,而在于:

什么时候调用;
对哪里调用;
用多大容差;
修复后怎么验收;
失败时怎么回滚;
是否应该让用户确认。

如果把这些接口放到一个修复系统里,我现在更倾向于这样理解它们的分工:

BRepCheck_Analyzer:
    基础体检工具,用于判断 OCCT 层面的 shape 有效性。

ShapeFix_Shape:
    轻量修复入口,适合基础清理,不适合承担复杂决策。

ShapeFix_Face / ShapeFix_Wire:
    面和 wire 层面的局部修复工具,适合配合问题定位使用。

ShapeFix_Wireframe:
    线框缺陷处理工具,对小边和 wire gap 有用,但要注意误删真实特征。

BRepBuilderAPI_Sewing:
    连接已有面,适合面集合缝合和局部 patch 后重新连接,不是补面算法。

ShapeAnalysis_FreeBounds:
    自由边和边界识别工具,常作为 open shell、gap 和补面流程的入口。

BRepFill_Filling:
    从边界生成修补面,适合局部缺面修复,但风险高,需要验证和用户确认。

这也是我后来对几何修复模块的一个基本判断:

导入阶段要克制;
基础修复要保守;
复杂修复要可解释;
局部修复要可回滚;
面向网格的修复还要考虑后续共形和稳定性。

从这个角度看,ShapeFixSewingFilling 不是几何修复的全部,而是修复系统中的几个基础执行器。

真正能让修复流程稳定下来的,是前面的检测、分类、策略选择,以及后面的验证和报告。