Technical note

从特征边到约束构面

记录一次 OCCT 约束构面的工程实现:为什么 Boundary、Guide 和 Crease 不能混在一起,以及构造一张可验收的面之前需要哪些门禁。

背景:有一组边,不等于能构成一张面

上一篇整理了特征边提取:从一张 B-Rep 面中得到外边界、内边界、拾取位置的 U/V 等参数线,以及带来源和参数信息的曲线约束。

得到这些边以后,很自然的下一步是:

能不能用它们重新构造一张面?

从接口调用上看,OCCT 提供了 BRepBuilderAPI_MakeFaceBRepFill_Filling 等工具,似乎只要把 Edge 放进去就可以生成 Face。

但实际工程里,最难的不是调用 Build()

真正难的是先回答:

哪些边负责裁剪面的外轮廓?
哪些边只是内部形状约束?
哪些边代表必须保留的尖锐折痕?
边界是否能按容差组成唯一闭合 Wire?
内部引导线是否和边界、支撑面一致?
单张光顺 B-Spline 面能不能表达当前约束网络?
生成成功以后,怎么判断结果可以提交?

如果不区分这些语义,把所有边都无差别地交给 Filling,结果很容易失控。

所以这次构面功能的重点,是在生成 Face 之前先建立明确的曲线角色和门禁。

三种曲线角色分别解决什么

当前约束体系把曲线分成三种角色:

Boundary
Guide
Crease

这三个角色不能混在一起,因为它们对曲面拓扑和几何形状的影响完全不同。

Boundary:决定面在哪里结束

Boundary 是最终 Face 的裁剪边界。

它通常来自:

原面 outer trimming edges;
原面 inner trimming edges;
用户重新分类的轮廓 Edge;
区域修复中识别到的缺口边界。

Boundary 回答的是:

这张面的参数曲面最终保留哪一块区域?

在当前单张光顺面流程里,Boundary 必须组成一个唯一的外轮廓 Wire。

如果边界不闭合、分成多个互不连接的 Wire,或者存在分支,就不能继续按单张面提交。

Guide:影响面的内部形状

Guide 是内部光顺约束,不裁剪最终结果。

它通常来自:

拾取位置的 U/V 等参数线;
平面截线;
投影到目标区域的用户曲线;
需要保持的内部走向或截面控制线。

Guide 回答的是:

在外边界已经确定以后,曲面内部应该怎样经过或贴近这些曲线?

Guide 可以影响曲面形状,但不能承担闭合 Boundary 的职责。

一组开口 Boundary 加上一条连接两端的 Guide,仍然不是一个合法的外轮廓。

Crease:要求曲面在这里不光顺

Crease 表示尖锐分隔线。

它回答的是:

结果应该在这里保持一条折痕,而不是跨过去形成一张连续光顺面。

这类约束无法由一张普通的光顺 B-Spline patch 正确表达。

更合理的做法是:

用 Crease 对曲线网络分区;
为每个子区域分别构造一张面;
让相邻 patch 共享同一条 Crease Edge;
分别控制每侧的连续性;
最后进行多面 Sewing 和区域回装。

当前版本尚未实现通用曲线网络分区和多 patch 重建,所以只要输入中包含 Crease,单张面构造器就应该明确拒绝,而不是生成一张错误的光顺面。

这是安全门禁,不是功能失败。

从 FeatureCurve 到 SurfaceConstraint

特征边提取阶段得到的是 FeatureCurve。

进入构面阶段后,它会转换成 SurfaceCurveConstraint,保留:

TopoDS_Edge;
SupportFace;
Boundary / Guide / Crease 角色;
连续性等级;
是否为硬约束;
可读标签。

这一步的意义,是把“曲线是怎么提取出来的”和“曲线在构面中怎么使用”连接起来。

默认情况下:

OuterBoundary / InnerBoundary -> Boundary
UIso / VIso / PlaneSection    -> Guide

应用层也可以在持久化以后重新分类。

例如用户可能把某条原本作为 Guide 的投影曲线改成 Crease,要求未来的多 patch 重建沿它分面。

如果应用层没有保存结构化元数据,而只把 Edge 传入旧接口,为了兼容,系统只能把这些预选 Edge 全部当成 Boundary。

这可以保证旧流程仍然能用,但会丢失 Guide 和 Crease 语义。

因此推荐的数据传递方式始终是结构化约束,而不是裸 Edge 列表。

第一道门禁:Boundary 必须组成唯一闭合 Wire

构面之前,系统先收集全部 Boundary Edge。

这些边不要求用户按拓扑顺序选择,构造器可以按连接容差尝试组装 Wire。

当前接受条件应该非常明确:

连接结果恰好只有一个 Wire;
Wire 类型有效;
Wire 在 B-Rep 意义下闭合。

如果得到多个 Wire,就说明当前输入并不是一个明确的单 patch 外边界。

如果 Wire 仍然开放,也不能让 Guide 帮它“隐式闭合”。

为什么当前只允许一个外轮廓?

因为一个带内孔的面至少需要:

一个 outer wire;
一个或多个 inner wires;
明确的内外环分类;
一致的方向和参数域裁剪;
Filling 支撑面与最终 trim face 之间的组合。

当前版本先完成“一个闭合外边界 + 内部 Guide”的单 patch 链路。

多 loop 和孔洞重建仍然需要单独扩展,不能因为 Boundary 提取能识别 inner wire,就声称构面阶段已经支持任意孔洞拓扑。

连接容差不能随便放大

Boundary Edge 的端点可能存在微小偏差,因此连接时需要容差。

容差太小,原本应该连接的边会被拆成多个 Wire。

容差太大,不应该相连的端点可能被错误吸附,导致:

边界顺序错误;
形成错误闭环;
局部短边被吞并;
真实间隙被掩盖;
生成的面跨过不应该跨越的区域。

所以连接容差不能被当成“调大直到能成面”的万能旋钮。

更合理的产品流程应该能够:

显示当前连接容差;
报告最大端点间隙;
区分自动连接和用户确认连接;
超过安全范围时拒绝自动闭合;
在区域修复中结合 Anchor 和模型尺度约束容差。

容差是工程必要项,但容差放大也是风险来源。

构面失败时,有时候正确做法不是继续放大容差,而是告诉用户边界本身不可靠。

Boundary 和 Guide 如何加入 Filling

Wire 构造成功后,系统按 Wire 顺序遍历 Boundary Edge,并把它们加入 BRepFill_Filling

如果 Boundary 有可靠的 SupportFace,可以把 Edge、支撑面和连续性一起交给 Filling:

filling.Add(edge, supportFace, continuity, Standard_True);

最后一个参数 Standard_True 表示这是一条边界约束。

如果 Boundary 只有裸 Edge,没有可靠 SupportFace,系统不应该假装拥有切向或曲率场,而是保守地按 C0 加入:

filling.Add(edge, GeomAbs_C0, Standard_True);

这里体现了一个比较重要的原则:

有支撑面证据时,才谈 G1/G2 连续性;
只有空间曲线时,默认只保证位置连续。

否则系统可能给 Filling 提出没有几何依据的高阶连续要求,既增加不收敛风险,也让结果误差难以解释。

Guide 不参与裁剪,所以加入 Filling 时使用:

filling.Add(edge, supportFace, continuity, Standard_False);

或者在没有支撑面时:

filling.Add(edge, GeomAbs_C0, Standard_False);

Standard_False 表示这条 Edge 是内部约束,而不是最终边界。

Guide 的价值主要体现在复杂非平面边界中。

只靠外轮廓构造曲面时,同一个边界可能对应很多不同的内部形状。增加 U/V、截面线或用户投影曲线,可以给 Filling 更多几何信息,让结果更接近原设计意图。

但 Guide 越多不一定越好。

如果多条 Guide 互相冲突,或者与 Boundary、InitialFace 不一致,算法可能无法收敛,或者生成局部振荡和扭曲。

所以 Guide 应该是经过筛选的设计约束,而不是把所有可见曲线都塞进构造器。

为什么 Crease 不能被当成普通 Guide

如果把一条 Crease 作为普通 Guide 加入单张 Filling,算法可能会让曲面经过它,但仍然在曲线两侧保持一张连续曲面。

这和“沿该线存在尖锐折痕”的语义完全不同。

真正的 Crease 需要两侧至少存在不同的 patch,使共享边上的法向可以发生跳变。

因此当前单面构造器应该在进入 Filling 之前就检查 Crease:

只要存在 Crease:
    要求进入 multi-patch 流程;
    不执行单面 Filling;
    返回明确说明。

这比“先生成一张看起来能用的面,以后再处理折痕”更安全。

因为一旦错误光顺面被提交,后续系统很难知道原来哪条线应该分隔 patch。

构面参数不能混成一个 tolerance

构面工具通常会涉及多类参数。

有些参数影响 Filling 的离散和迭代,例如曲线离散点数、迭代次数、能量项等。

有些参数影响近似复杂度和误差,例如三维容差、二维容差、角度容差、曲率容差、最大近似阶数和分段数。

还有一类参数专门用于拓扑连接,例如 Boundary 连接容差。

这几类参数不能全部混成一个统一的 tolerance

因为调大某个容差,到底是在改变边界连接、曲面逼近,还是连续性验收,上层必须知道。

我更倾向于把它们分开理解:

边界连接参数:
  决定 Boundary 能否组成合法 Wire。

曲面构造参数:
  决定 Filling 如何逼近约束并生成曲面。

结果验收参数:
  决定结果 Face 是否值得继续进入修复事务。

只有这样,失败时才能给出更具体的原因,而不是简单告诉用户“构面失败”。

构造和验收流程

所有约束准备好以后,系统才调用 Filling。

一个更稳妥的流程是:

检查 Crease

连接 Boundary,得到唯一闭合 Wire

创建 Filling

按 Wire 顺序加入 Boundary

加入内部 Guide

必要时加入点约束

Build

检查 IsDone

取得 result Face

B-Rep 有效性验证

记录 G0 / G1 / G2 Error

这里要注意:IsDone() 为 true,只说明 Filling 算法完成。

它不等于结果可以提交。

如果 result Face 为空,不能提交。

如果 B-Rep 检查不通过,不能提交。

即使 B-Rep 有效,G0/G1/G2 Error 也应该进入预览和结果报告,帮助用户判断约束满足程度。

构造出一张 Face,只是得到了候选几何。

它还没有自动变成原模型的一部分。

FillFace 交互里的角色表达

交互式构面应该让用户明确当前输入角色。

例如:

B = Boundary
G = Guide
C = Crease

用户点击 Edge 时,这条边会进入当前角色集合。

如果同一条 Edge 已存在于其他角色,系统应该先从原集合移除,避免一条边同时被解释成 Boundary 和 Guide。

界面预览也应该分层:

Boundary edges;
Guide edges;
Crease edges;
当前生成的 Face preview。

每次约束变化后都重建预览。

只有在以下条件满足时,才允许提交:

已经生成有效 Face preview;
结果 Face 非空;
构面结果成功;
Crease 集合为空。

如果包含 Crease,提示应该明确说明需要多 patch 分区,不能把当前结果作为一张面提交。

这类限制看起来保守,但对几何软件很重要。构面失败不可怕,生成一张含义错误但看起来可用的面更危险。

特征边结果怎样直接交给构面

上一篇里的特征边提取结果保留了完整 FeatureCurve。

推荐的交接方式是把它转换为 SurfaceCurveConstraint,再传入构面命令。

结构化输入会覆盖旧的“全部预选 Edge 都是 Boundary”的规则。

这样:

OuterBoundary / InnerBoundary 继续作为 Boundary;
UIso / VIso 继续作为 Guide;
未来持久化的 Crease 继续触发 multi-patch 门禁。

如果应用层只传普通 Edge 或 Wire,旧流程可以兼容,但所有输入会先按 Boundary 处理。

这也是为什么特征曲线元数据应该进入模型树或数据库,而不是提交后只剩下一个 Edge shape。

一旦元数据丢失,后续构面就只能重新猜测这些边的角色。

生成一张面以后还没有完成修复

约束构面当前产生的是一张新 Face。

它可以用于:

独立曲面建模;
补面预览;
区域重建候选;
后续 Sewing 输入;
多 patch 流程中的一个子面。

但如果目标是修复原模型中的孔洞或坏面,还需要继续处理:

新 Face 是否和周围 Anchor 对齐;
是否与邻域面建立共享边;
是否需要 Sewing;
原问题 Face 或开口区域如何替换;
root shape 是否仍然有效;
free edge 是否减少;
是否新增 non-manifold;
材料、边界条件和命名选择如何迁移;
网格能否重新生成。

所以“特征边构成面”和“区域化模型修复”是相邻但不同的两层。

前者负责生成候选几何。

后者负责把候选几何安全放回模型,并决定能否提交。

当前能力边界

当前构面链路需要明确几个边界。

当前只支持一个光顺外边界 patch

一个连通闭合 outer wire 是当前单面构造的硬条件。

带多个 inner wire 的孔洞拓扑还需要多 loop 支持和最终 trim 处理。

Crease 只识别,不执行多 patch 重建

当前可以保存和显示 Crease,并明确阻止单面提交。

真正的曲线网络分区、每个 cell 构面、共享 Crease Edge 和多面 Sewing 尚未完成。

Guide 冲突还缺少独立诊断

当前主要依赖 Filling 是否收敛和最终误差判断。

后续可以在 Build 之前增加 Guide-Guide、Guide-Boundary 的相交、距离和方向一致性分析,给出更具体的失败原因。

点约束没有进入 UI

底层结构和 Filling 调用可以支持点约束,但当前用户流程仍然是 feature-edge-only。

Face 有效不等于网格一定可用

基础 B-Rep 检查通过只是门禁之一。

如果结果要进入 CAE 前处理,还需要曲面质量、最小特征、边界连续性和网格试剖验收。

一个更完整的构面工具流

如果把当前能力放在完整工程流程里,我会这样组织:

提取或选择特征曲线

保留 FeatureCurve 来源和角色

用户确认 Boundary / Guide / Crease

检查 Boundary 数量、连接性和闭合性

发现 Crease?
    ├─ 是:进入多 patch 规划,不执行单面 Filling
    └─ 否:继续

检查 Guide 和支撑面证据

构造候选 Face

B-Rep 与 G0/G1/G2 误差验收

显示真实几何预览

作为独立 Face 提交,或交给区域修复事务

Sewing / 局部替换 / 全局验证 / 网格复查

这条流程没有把 Filling 当成万能补面算法。

它把 Filling 放在一个明确的位置:根据已经分类的曲线约束生成候选光顺面。

小结

从特征边构造一张面,真正关键的不是 Edge 数量,而是每条 Edge 的职责。

Boundary 决定结果 Face 的裁剪范围;
Guide 影响曲面内部形状,但不负责闭合;
Crease 要求分成多张 patch,不能塞进一张光顺面;
SupportFace 决定是否有依据提出 G1/G2 连续性;
连接容差决定 Boundary 能否形成唯一闭合 Wire;
G0/G1/G2 Error 和 B-Rep 检查决定候选是否值得继续使用。

所以这次构面功能的核心,不是“把几条边 Add 到 Filling”,而是先把曲线网络变成一组可解释、可验证的约束。

当角色、来源和门禁都明确以后,生成的 Face 才适合进入下一层工程流程。

下一篇可以继续讨论这张候选面如何进入区域化修复:问题怎样被聚成区域,真实几何预览怎样通过验收,以及为什么提交必须使用预览中的同一份结果,而不能重新计算一次。