做CAD二次开发或者三维参数化建模的时候,经常绕不开一个需求:把模型里的实体、面拆开,把它们的边线单独提出来,拿去做后续处理。比如生成二维工程图轮廓、做CNC刀路边界、做有限元网格的边界约束,甚至单纯为了把模型线框可视化。前阵子我正好在搞一套通用几何工具库,核心功能就是“提取实体、面的边线获得边缘系列函数”。折腾下来发现这里面细节不少,坑也挺多,干脆整理成一篇实操向总结,给遇到同类需求的朋友做个参考。
这篇文章适合正在写CAD插件、搞参数化建模脚本,或者被“怎么拿到模型边缘”这个问题卡住的人。不管你用的是什么平台,只要底层基于B-rep边界表示模型,思路基本都通用,我下面讲的方案可以直接迁移过去。
1. 需求拆解与整体设计思路
1.1 为什么不能只靠“自带取边”功能
很多建模软件和二次开发库都提供了取边的接口,比如直接遍历Shape.Edges或者调用某个API就能拿到边线。但实际项目里,这种“裸取”往往不够用,原因有几个:
第一,边线的顺序是乱的。实体内部存储拓扑时,边线存放顺序取决于建模过程,跟用户期望的“按轮廓走一圈”完全无关。直接拿出来的边线可能首尾不相连,方向也忽左忽右,用在加工路径或者图纸标注里会导致严重问题。
第二,分类不清晰。一个面可能既有直线边又有圆弧边,还有样条曲线边。不同用途需要的边类型不一样:做拉伸特征可能只要直线段,做圆角识别只关心圆弧段,做自由曲面边界的又要样条数据。一次全部返回,后面还得自己做类型过滤,很麻烦。
第三,没有做重复清洗。相邻两个面共享的边会重复出现,不处理的话后续计算边数、统计长度都会出错。
所以我决定写一套独立的“边缘系列函数”,不是简单封装一次遍历,而是做成一整套围绕“边缘”的完整工具链。它的输入是实体或面,输出是一组有序、分类清晰、可供业务直接调用的边线对象。
1.2 B-rep 模型的基础结构:点、边、面、环
要写对提取函数,得先理解B-rep模型的数据组织方式。可以把它想象成一套俄罗斯套娃:实体(Solid)是由多个面(Face)围成的封闭空间;每个面有自己的边界,边界由若干条边(Edge)按顺序首尾相连;每条边的两头是顶点(Vertex)。
这里的“环”(Wire或Loop)很关键。一个面可以有好几个环,外轮廓是一个环,如果上面打了一个孔,孔洞的边界就是另一个环。提取面的边线时,如果不区分环,就会把外轮廓和孔洞边缘混在一起;如果区分,就能方便地识别“外边界”和“内边界”。
我设计功能的时候,把“环”的概念作为一等公民,因为后面很多应用都依赖它。比如生成2D轮廓,外环直接是封闭轮廓;做减重孔识别,内环就是孔位。函数库里特意提供了返回环信息的版本,就是为了应对这种场景。
1.3 工具选型的逻辑:基于开源内核还是商业API
我用的是基于开源几何内核的Python环境来验证这套函数。选它不是因为商业平台不好,而是因为开源内核的控制粒度更细,能清楚地看到每条边对应的几何曲线类型、容差、朝向标志位,调试起来方便。
如果你的环境固定在某商业CAD软件上,也不用担心。大部分商业二次开发接口都提供了等价的拓扑遍历能力,只是命名风格不同。我下面给出的逻辑和算法是通用的,翻译成对应平台的接口就行。核心思路就是:从拓扑容器拿边对象,从边对象拿底层几何曲线,再结合边的朝向信息把曲线整理成有序序列。
2. 边缘系列函数的核心设计与API说明
2.1 函数族总览
我最终实现的“边缘系列函数”不是一个单独的函数,而是一组配合使用的API。它们覆盖了几种最常见的取边需求:
| 函数名 | 输入 | 输出 | 适用场景 |
|---|---|---|---|
| get_edges_from_solid(solid) | 实体 | 所有边的列表 | 整体线框预览、全局边数统计 |
| get_edges_from_face(face) | 单个面 | 该面的所有边 | 局部轮廓提取、贴标签 |
| get_wires_from_face(face) | 单个面 | 环的列表,每个环含有序边组 | 外轮廓/内孔分离、路径排序 |
| get_edges_by_type(solid, edge_type) | 实体+类型 | 过滤后的边表 | 直线段提取、圆弧圆角识别 |
| get_edge_curve(edge) | 边 | 对应几何曲线对象 | 获取参数方程、做延伸裁剪 |
| get_edge_vertices(edge) | 边 | 两个端点 | 计算长度、连接关系判断 |
| fix_edge_direction(edge, ref_point) | 边+参考点 | 调整方向后的边 | 统一边线走向,处理法线向量 |
这套结构是逐步演进的。第一版我只写了一个“取所有边”的函数,用不了多久就发现不行,因为业务方真正需要的不是“一堆边”,而是“能直接画轮廓的边”。于是又补了环排序、方向统一这层。每次补丁都是在真实需求驱动下加的,不是一套设计定死了从头堆。
2.2 各函数内部关键逻辑
以get_edges_from_face为例,内部逻辑不复杂但有几个细节:
先拿到面对象,然后遍历其所有环。每个环内部按拓扑顺序排列着边线。这里要注意,一个面如果是平面,它的环方向依赖面法向量的约定;如果是曲面,还要注意UV坐标系的走向。大部分情况下,直接从环里取出来的边对象是Base对象,还没计算具体几何,要单独调用“获取曲线”接口才能得到真正的线、圆或样条。
get_edges_by_type这个函数我做了类型枚举:直线、圆弧、椭圆弧、样条、以及其他。判断依据不是边对象本身,而是底层曲线的类型Id。实测中经常遇到“明明画的是圆弧,取出来变成样条”的情况,原因在于建模者用了拟合方式生成曲线。如果业务必须识别圆弧,建议在函数里加一个“近似检测”选项:允许把接近圆弧的样条曲线重采样拟合成圆弧。这个功能对圆角识别特别有用。
方向统一那一层,是这套函数里最有价值的模块。我的做法是:取边中点和参考向量做点积判断,如果方向不对就反转边的逻辑方向。注意这里不能直接修改拓扑对象,因为会破坏模型的引用一致性。正确做法是返回一条方向反转后的“逻辑边”包装对象,而不是直接改底层的边。
2.3 一个让函数通用的关键设计:边线包装对象
写代码最怕的就是绑定某个特定API不可自拔。为了在这套函数里保持统一,我给边线设计了一个轻量包装对象,里面存三样东西:原始拓扑边、对应的几何曲线、逻辑方向标志位。
这样做的最大好处是:调用方不需要关心底层是哪个平台,只要用包装对象的结构就行。几何计算也好做,因为曲线对象直接内置在包装里,不用每次重新去内核里取。代价也是有的——多了一层内存占用,不过对于常规模型量级,完全可接受。
我建议任何要在多个脚本里复用“边缘提取”功能的人,都先做一层包装抽象,不要直接用裸边对象到处传。
3. 实操过程与核心环节实现
3.1 基础环境准备与通用遍历模板
我先把整套函数最底层的“遍历实体所有边”模板贴出来,这是一个不依赖复杂数据结构就能跑起来的起点。
def collect_edges(shape): """从任意B-rep形状对象中收集所有边线""" edges = [] # 如果形状本身就是一条边 if shape.shape_type == "edge": edges.append(shape) # 如果是面,则取面的所有边界环 elif shape.shape_type == "face": for wire in shape.wires: for edge in wire.edges: edges.append(edge) # 如果是实体,则先取面,再递归取边 elif shape.shape_type == "solid": for face in shape.faces: for wire in face.wires: for edge in wire.edges: edges.append(edge) return edges这个模板看起来简单,但它隐含了一个重要细节:用“面 → 环 → 边”的路径去遍历,而不是直接用“实体 → 边”的扁平接口。原因有两层。第一层,保留环归属信息,方便后面做内外轮廓判断;第二层,不会漏掉面上的孤立岛边。不同平台可能支持直接Shape.Edges扁平遍历,但那种方式不保留面的归属关系,对后续筛选很不利。
3.2 关键实现:提取某个面的边线并排序成环
面的边线排序是一个容易踩坑的点。我实际写的第一版排序算法,直接按“当前边的终点要找下一条边的起点”这个规则去连,结果在遇到T形连接(三条边交汇于一点)时彻底卡住,因为一个点可能对应多个候选边。
解决办法是引入“环”的信息,而不是单纯依赖端点空间位置匹配。每个面内的环已经由内核按正确的拓扑顺序组装好,我只需要把环里的边顺着物理顺序取出来,不用自己做排序匹配。如果平台的接口不支持直接读环,再考虑用端点连接法,并做并查集处理多分支。
def get_sorted_edges_from_face(face): """返回面的外环和内环,每个环包含按顺序排列的边线""" loops = [] for wire in face.wires: loop_edges = [] for edge in wire.edges: loop_edges.append(edge) if not loop_edges: continue loops.append(loop_edges) return loops输出是“环的列表”,环与环之间的边界就是外轮廓和内孔的分界。判断哪个是外环,可以算环的有向面积,面积最大的通常就是外环。这个方法在平面面上稳定可靠,但在曲面面上需要投影后计算,处理稍复杂。实际操作里,我九成五以上的场景都是平面面,所以用面积排序就够用了。
3.3 参数计算:边长度、路径总长与连续性判断
拿到有序边线后,很多场景还要求计算边长、判断首尾是否衔接、求路径总长。这里我写了几个辅助参数计算,顺便说说背后的原理。
边线长度计算理论上是对曲线参数区间求积分。直线段直接用两点距离;圆弧用半径乘以圆心角;样条没有解析解,要用数值积分,或者用离散点逼近。实测下来,样条的数值积分在精度要求不高的场合完全够用,但如果要做数控加工,千万不能用离散逼近代替真实曲线长度,必须走底层内核的高精度曲面/曲线求长接口。
连续性判断方面,我做了一个接口,返回相邻两条边的端点连接状态:完全重合、误差内重合、存在间隙、有重叠。这个接口对后续做轮廓闭合检查很有用,能够提前发现模型质量差导致的缝隙问题。
def edge_length(edge): """根据曲线类型计算长度""" curve = edge.curve if edge.is_line: x1, y1, z1 = edge.vertex0 x2, y2, z2 = edge.vertex1 return ((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2) ** 0.5 elif edge.is_arc: return abs(edge.radius * edge.angle) elif edge.is_spline: # 调用底层数值积分 return curve.length_by_arc_length(edge.param0, edge.param1) return 0.03.4 圆角提取和轮廓切断:两个进阶实用功能
做机械零件识别的时候,经常需要把圆角边从轮廓里单独提出来。传统做法是遍历所有边,看底层曲线是不是圆弧。但这套方法有两个漏洞:一是倒角可能是样条拟合出来的,二是圆弧边可能出现在非圆角区域(比如半圆腰型槽)。
我给这套函数加了“上下文圆角识别”能力:只有连续的直线边之间夹着的短圆弧才被判定为圆角。判定条件有三个:圆弧半径在设定范围内、圆弧两侧都连接直线边、圆弧角度小于180度。用这三个条件过滤后,误判率大幅下降。
另一个实用功能是“按交点切断长边”。有些模型里一条轮廓边很长,需要按若干特征点把它切成多段,比如做焊缝分组或者贴标签定位。我实现的方法是把曲线按参数均分,再根据投影点反求参数。这个功能在提取轮廓做工艺标记时特别常用。
4. 典型应用场景与后续扩展方向
4.1 二维轮廓输出和工程图注记
最直接的应用就是把三维面的边线转成二维轮廓。做法是:提取面的所有环 → 选择外环 → 用平面投影把三维曲线映射到指定平面 → 输出成DXF或SVG格式。这套流程里边线排序的稳定性直接决定成图质量,顺序一旦乱掉,轮廓就变成各种交叉线。
我建议做这一步时,优先用面自身的坐标系。面都有参数化的UV空间,把边线从三维转到UV平面输出,效果比任意平面投影标准得多。用面坐标系的好处还有一点:遇到圆柱面、球面这种曲面,UV展开后能直接输出钣金展开图,后期做下料尺寸计算特别方便。
4.2 加工路径边界的自动生成
数控加工里走轮廓前要选边界曲线。以前的做法是人工在CAD界面里一条条点选,效率非常低。用这套函数可以直接读入实体,筛出指定高度的所有水平边,再按环顺序排好,直接生成一刀走完的轮廓线。
这里要提醒一个实际经验:加工路径的轮廓必须闭合连续,不能有微小断缝。如果实体建模时面之间拼接精度不够,提取出的边线可能存在0.01毫米级别的间隙,肉眼看不到,但机床识别时会断刀。我因此在函数库里专门加了一个“闭合修复”选项,对间隙小于设定容差的相邻边,自动延伸或合并端点。
4.3 网格处理和模型简化的数据预处理
做CAE前处理时,网格划分软件经常需要指定约束边、载荷边或固定边。手动选取极其痛苦。反过来,直接在CAD阶段生成带标签的边线集合,导出时附加属性,就能实现“参数化选边”。
我实际做过一个项目里的应用:把流体域的入口面、出口面的边分别提取出来,自动标记成两组特定颜色和名称,再交给网格软件识别。这样做之后,模型迭代时不用重新手动选边,节省了大量重复劳动。模型再复杂也不会漏选——只要拓扑存在,函数就一定能提出来,不像人眼容易在密集模型里看漏线。
5. 常见问题与排查技巧实录
5.1 边线数量对不上?先查重复边和内部边
实际使用中,用户反馈最多的一个问题就是:“提取出的边比预期的多很多。”原因一般是两个:第一,相邻面共享边重复计数;第二,实体内部的一些辅助曲面也贡献了边,这些边并不在外部轮廓上。
排查方法很简单:把提取到的所有边按端点坐标排序,然后两两比对端点是否相同,相同的就是重复边。内部边则可以通过一个启发式判断——内部边一般是两个面的边界,而外部轮廓边只有一个面与之相邻。用“面引用计数等于1的边就是实体外轮廓边”这条规则去过滤,通常能处理干净。
5.2 提取圆弧边失败变成样条
第二种高频问题:CAD里明明画的圆弧,提取出来却是样条曲线。这多半发生在模型经过格式转换或特征拟合之后。核心里有“几何重建”的步骤,会把原始解析曲线离散成样条。
处理方式我在前面提过,就是用近似拟合恢复。具体算法也不复杂:按给定精度把样条密集取点,然后用三点定圆法判断点列是否共圆。误差在容差内就直接用拟合圆替代样条。注意一点,容差设置要跟CAD软件自身精度匹配。有些图纸精度是0.01毫米,容差设太小就拟合不到圆,设太大又会把本应是样条的曲线误判为圆弧。
5.3 方向不一致导致路径反向
低版本算法里经常出现的问题:同一批边,第一次提取方向是顺时针,第二次就变成了逆时针。原因在于底层的方向信息依赖边在两边的“朝向”标志位,而这个标志位会受面顺序的影响,每次重新打开或重建模型都可能变化。
解决办法是:在提取边之后强制做一次方向统一,以某个基准边方向为参照,遍历整个环,保证相邻边“终点接起点”。这个排序不只是为了好看,对加工路径正转反转、线宽补偿方向、倒角偏向性都有直接影响。我在函数库里预留了一个direction_mode参数,支持“按线框视觉统一”和“按几何连续性统一”两种策略,前者多用于可视化,后者多用于工程计算。
5.4 曲面上取边不闭合:精度容差调整
曲面上的边线,特别是样条曲面边界,经常出现“看着是封闭的,取出来数据却有缝”的情况。这主要是曲面边界线和相邻面的边界线在数学描述上参数有细微差异,属于SPA内核容差问题。
调试时我会先输出各个端点坐标,把误差量化。如果误差在系统容差范围内,就可以视为闭合,继续使用。如果超出,我的函数会强制把两个端点距离拉平,做一次小范围的几何修复。实际操作经验是:不要盲目调高系统全局容差,那会导致真正的几何缺陷被掩盖。先在局部对边做修复,修复不了的再回到建模环境检查原始曲面。
最后再分享一个让我很受用的技巧
这套“提取实体、面的边线获得边缘系列函数”我在好几个不同项目里反复重写过,每一版的进步几乎都来自真实踩坑。最实用的改版是把“环”的概念引入结果组织,而不是只返回一长串边。这对后面所有环节都带来了优势:排序简单了、内外轮廓可区分了、闭合检查也容易了。
如果你刚开始做类似工具,建议不要一上来就追求大而全的框架,先用我前面给的模板跑通最基础的一条链路:实体 → 面 → 环 → 边。等这条链路稳定后,再加方向统一、类型过滤、圆角识别这些进阶能力。这样开发压力小,调试难度低,出活快。
后续我打算给这套函数继续扩展两个方向:一个是支持直接输出曲线到通用交换格式,另一个是增加对曲面边界的自动延伸和修剪处理。如果你也在搞类似的功能,或者遇到了我还没提到的奇葩边缘情况,欢迎一起交流经验和教训。