第10章:参数化截面与拉伸建模
上一章我们搭起了 CsgNode 树的骨架。本章深入声明式 API 中最贴近工程实用的部分——参数化二维截面 Profile2D 与拉伸节点 ExtrudeNode。这套机制专为型材建模而生:H 型钢、槽钢、方管、L 型角钢、梯形、胶囊形……只要给出截面参数和拉伸长度,就能生成对应的三维型材。我们还会讲清拉伸背后的耳切三角化原理,以及 WedgeNode 楔形体的构造。这两类节点是第 13 章”C 型钢檩条”实战的直接基础。
10.1 什么是拉伸建模
拉伸(Extrude) 是最古老也最实用的建模手法之一:取一个二维截面轮廓,沿垂直方向”拔”出一段长度,就得到一个等截面的三维柱体。生活中绝大多数型材——钢梁、铝型材、管子——本质上都是”某个截面拉伸一段”。
OpenCSG.NET 把这个思想拆成两块:
Profile2D——描述截面形状的参数配方(不是顶点,是”配方”,求值时才展开成顶点)。ExtrudeNode(Profile, Height)——把某个截面沿 +Z 方向拉伸Height高度。
拉伸方向是 +Z:
ExtrudeNode从z = 0拉到z = Height。请注意它和Cylinder(沿 Y 轴)的朝向不同——型材沿 Z 立起来。需要别的朝向时,用第 7 章的旋转或TransformNode调整。
10.2 Profile2D:七种内置截面
Profile2D 是一个抽象记录(定义在 CsgNode.cs,各子类在 Profiles.cs):
public abstract record Profile2D;
它有七个内置子类,每个都是一个带参数的 record。配套的 Profiles 静态类提供便捷工厂。下面逐一介绍(参数名和语义均来自源码的中文 XML 注释)。
10.2.1 RectangleProfile 矩形
最基础的截面,一个居中的矩形(中心在原点):
public record RectangleProfile(double Width, double Height) : Profile2D;
var rect = Profiles.Rectangle(width: 4, height: 2); // 4×2 的矩形截面
展开后是四个角点 (-W/2,-H/2) → (W/2,-H/2) → (W/2,H/2) → (-W/2,H/2),逆时针排列。
10.2.2 HBeamProfile 工字钢/H 型钢
工程中最常见的钢梁截面,由竖直腹板(web) 和上下水平翼缘(flange) 组成,形如”工”字:
public record HBeamProfile(double WebHeight, double FlangeWidth,
double WebThickness, double FlangeThickness) : Profile2D;
var hbeam = Profiles.HBeam(webHeight: 100, flangeWidth: 80,
webThickness: 10, flangeThickness: 12);
WebHeight:腹板高度(即总高减去两倍翼缘厚)。FlangeWidth:翼缘宽度。WebThickness:腹板厚度。FlangeThickness:翼缘厚度。
它展开成一个 12 个顶点的”工”字形轮廓,截面中心在原点,总高 = WebHeight + 2 × FlangeThickness。
10.2.3 ChannelProfile 槽钢/C 型钢
槽钢截面,形如”C”或”[“——一条竖直腹板加上下两条朝一侧伸出的翼缘:
public record ChannelProfile(double WebHeight, double FlangeWidth,
double WebThickness, double FlangeThickness) : Profile2D;
var channel = Profiles.Channel(webHeight: 100, flangeWidth: 60,
webThickness: 8, flangeThickness: 10);
参数含义与 H 型钢一致,区别在于翼缘只朝一侧伸出,形成开口的槽形。展开为 8 个顶点。第 13 章的 C 型钢檩条正是基于这一类思想(但用命令式布尔实现更复杂的冷弯细节)。
10.2.4 SquareTubeProfile 方管(重要特例)
方管是空心的矩形管。但这里有一个关键设计——源码注释写得很清楚:
“方管截面(空心矩形)。仅返回外轮廓,空心效果通过 CsgNode 层
SubtractNode实现。”
也就是说,SquareTubeProfile 展开时只给出外矩形轮廓,Thickness(壁厚)参数在截面展开阶段并不会自动挖空。要得到真正的空心方管,你需要自己用布尔减去一个内截面:
double W = 80, H = 80, t = 5;
// 外管拉伸
var outer = CsgNodes.Extrude(Profiles.SquareTube(W, H, t), height: 50);
// 内孔拉伸(比外管略"高"一点以保证穿透,或等高按需)
var inner = CsgNodes.Extrude(Profiles.Rectangle(W - 2 * t, H - 2 * t), height: 50);
// 用声明式 SubtractNode 挖空
var hollowTube = CsgNodes.Subtract(outer, inner);
var solid = CsgEvaluator.Evaluate(hollowTube);
记住这个特例:SquareTubeProfile 的 Thickness 更多是”记录设计意图”的元数据,实际挖空要靠你在节点层做差集。
10.2.5 TrapezoidProfile 等腰梯形
顶边和底边都平行于 X 轴的等腰梯形:
public record TrapezoidProfile(double TopWidth, double BottomWidth, double Height) : Profile2D;
var trap = Profiles.Trapezoid(topWidth: 2, bottomWidth: 3, height: 2);
底边在 y = 0、顶边在 y = Height,四个顶点 (-Bot/2,0) → (Bot/2,0) → (Top/2,H) → (-Top/2,H)。
10.2.6 CapsuleProfile 胶囊形
一个矩形两端各接一个半圆,长轴沿 X 方向——像药丸/跑道形:
public record CapsuleProfile(double RectWidth, double Radius) : Profile2D;
var capsule = Profiles.Capsule(rectWidth: 3, radius: 1);
RectWidth:中间矩形部分的宽度。Radius:两端半圆的半径(也等于截面的半高)。
它是唯一带曲线的截面,展开时用 segs = 16 段折线逼近每个半圆,所以顶点数较多(约 34 个)。半圆分辨率固定为 16,需要更光滑时目前只能改源码。
10.2.7 LShapeProfile L 型角钢
L 形(角钢)截面,竖边沿 Y、水平边沿 X:
public record LShapeProfile(double Vertical, double Horizontal, double Thickness) : Profile2D;
var lshape = Profiles.LShape(vertical: 3, horizontal: 2, thickness: 0.5);
Vertical:竖边高度。Horizontal:水平边宽度。Thickness:两条边的厚度。
从原点出发展开为 6 个顶点,构成一个直角”L”。
10.2.8 截面一览表
| 截面 | 工厂 | 参数 | 顶点数 | 备注 |
|---|---|---|---|---|
| 矩形 | Rectangle(w, h) |
宽、高 | 4 | 中心在原点 |
| H 型钢 | HBeam(wh, fw, wt, ft) |
腹板高/翼缘宽/腹板厚/翼缘厚 | 12 | 总高=wh+2ft |
| 槽钢 | Channel(wh, fw, wt, ft) |
同上 | 8 | 开口朝一侧 |
| 方管 | SquareTube(w, h, t) |
外宽/外高/壁厚 | 4 | 只给外轮廓,挖空靠布尔 |
| 梯形 | Trapezoid(top, bot, h) |
顶宽/底宽/高 | 4 | 底在 y=0 |
| 胶囊 | Capsule(rw, r) |
矩形宽/半圆半径 | ~34 | 半圆 16 段 |
| L 型 | LShape(v, h, t) |
竖高/横宽/厚 | 6 | 从原点展开 |
10.3 ExtrudeNode:拉伸求值管线
ExtrudeNode(Profile2D Profile, double Height) 的求值(CsgEvaluator.EvaluateExtrude)分四步,理解它能帮你判断”为什么某个自定义截面拉伸失败”:
- 展开截面:
ExpandProfile(profile)把配方变成一圈二维顶点(逆时针)。 - 耳切三角化:
Triangulate(pts)用耳切法把截面多边形拆成三角形(用于封顶封底)。 - 生成顶盖和底盖:把三角化结果分别放在
z = 0(底,法线朝 −Z,顶点顺序反转)和z = Height(顶,法线朝 +Z)。 - 生成侧面:沿截面每条边,在底和顶之间连出一个四边形侧壁。
最终 Solid.FromPolygons 把顶盖、底盖、侧壁合成一个封闭实体。
一个约束:拉伸高度必须为正,否则抛异常:
// Height <= 0 会抛 CsgEvaluationException
var bad = CsgEvaluator.Evaluate(new ExtrudeNode(Profiles.Rectangle(4, 2), 0));
10.3.1 基本用法
using Csg;
using static Csg.CsgNodes;
// 一根 50 长的 H 型钢
var hbeam = CsgEvaluator.Evaluate(
Extrude(Profiles.HBeam(100, 80, 10, 12), height: 50));
using var w = new StreamWriter("hbeam.stl");
hbeam.WriteStl("hbeam", w);
配合布尔与变换,型材可以进一步加工(钻孔、切断、拼接),这正是第 13 章的主题。
10.4 耳切三角化:把任意简单多边形拆成三角形
第 8 章说过,导出 STL 时对凸多边形用扇形三角化就够了。但截面轮廓(H 型钢、L 型、槽钢)大多是凹多边形,扇形法会出错。所以拉伸封顶/封底时,CsgEvaluator 用了更通用的耳切法(ear clipping)。
耳切法的直觉:反复从多边形上”剪掉一只耳朵”。所谓”耳朵”,是一个满足两个条件的顶点三元组 (prev, curr, next):
- 凸角:
curr处是凸的(IsConvex通过二维叉积判断,要求顶点逆时针 CCW)。 - 不含其它顶点:三角形
(prev, curr, next)内部不包含多边形的任何其它顶点(HasPointInside+PointInTriangle判断)。
每找到一只耳朵,就记录这个三角形并把 curr 从多边形里移除,如此循环直到只剩 3 个顶点。源码里还加了一个安全阀 safety = polygon.Count * 3,防止病态输入导致死循环。
10.4.1 对自定义截面的启示
如果你要扩展一个新的 Profile2D(第 14 章会讲怎么做),耳切法对你的顶点提出了要求:
- 顶点必须逆时针(CCW)排列——否则
IsConvex判断全反,耳切失败。 - 必须是简单多边形(不自交)——耳切不处理自交或带洞轮廓。带洞的截面(如真正的空心方管)要靠布尔差集实现,而不是塞进一个
Profile2D。
理解这两条,就能明白为什么 SquareTubeProfile 只给外轮廓、空心靠 SubtractNode——因为”带洞”本质上不是一个简单多边形,耳切法处理不了。
10.5 WedgeNode:楔形体
WedgeNode 是一个非拉伸的图元节点,生成一个”楔形/斜坡”——底面是矩形,沿 +Z 逐渐收窄成顶部的一条棱线(像一个三棱柱斜躺,或一段斜坡):
public record WedgeNode(Vector3D Corner, double Width, double Depth, double Height) : CsgNode;
var wedge = CsgEvaluator.Evaluate(
new WedgeNode(new Vector3D(0, 0, 0), width: 2, depth: 2, height: 2));
Corner:底面矩形的中心坐标。Width:X 方向宽度。Depth:Y 方向深度。Height:Z 方向高度。
其几何(EvaluateWedge)由 5 个面构成:一个矩形底面、两个梯形/矩形斜面、两个三角形端面。顶部棱线沿 X 方向,位于 Corner 正上方 Height 处、Y 居中。三个尺寸都必须为正,否则抛异常。
楔形常用于做倒角、斜撑、屋面坡、加强筋等。它可以和其它节点自由布尔,例如官方测试里就有”楔形 ∪ 立方体”:
var wedge = new WedgeNode(new Vector3D(0, 0, 0), 2, 2, 2);
var box = new BoxNode(new Vector3D(0.5, 0, 0), new Vector3D(1, 1, 1));
var solid = CsgEvaluator.Evaluate(CsgNodes.Union(wedge, box));
10.6 综合示例:一段空心方管上焊一块 L 型加强板
把本章的截面、拉伸、布尔组合起来,做一个小装配:
using Csg;
using static Csg.CsgNodes;
double L = 60; // 型材长度(沿 Z)
// 1) 空心方管:外管减内孔
var tube = Subtract(
Extrude(Profiles.SquareTube(40, 40, 4), L),
Extrude(Profiles.Rectangle(32, 32), L) // 40 - 2×4 = 32 的内孔
);
// 2) 一块 L 型角钢加强板,拉伸一小段,贴到方管一侧
var rib = Transform(
translation: new Vector3D(20, -20, 10), // 移到方管右下外侧
rotation: new Vector3D(0, 0, 0),
child: Extrude(Profiles.LShape(20, 20, 3), 12)
);
// 3) 合并
var assembly = Union(tube, rib);
var solid = CsgEvaluator.Evaluate(assembly);
using var w = new StreamWriter("assembly.stl");
solid.WriteStl("assembly", w);
这个例子体现了参数化型材建模的威力:改几个数字就能生成不同规格的方管和加强板,SubtractNode 干净地实现空心,TransformNode 负责定位。
10.7 本章小结
- 拉伸建模 = 二维截面
Profile2D沿 +Z 拉伸Height;型材沿 Z 立起,与Cylinder(沿 Y)朝向不同。 - 内置七种截面:矩形、H 型钢、槽钢、方管、梯形、胶囊、L 型,均有
Profiles.*工厂。SquareTubeProfile只给外轮廓,空心靠SubtractNode布尔实现。 ExtrudeNode求值分四步:展开截面 → 耳切三角化 → 顶/底盖 → 侧壁;高度必须为正。- 耳切三角化能处理凹的截面,但要求顶点逆时针、简单不自交——这也是”带洞截面必须用布尔而非单个 Profile”的根本原因。
WedgeNode生成楔形/斜坡(矩形底 + 顶部棱线,5 个面),三尺寸须为正,常用于倒角、坡面、加强筋。
下一章讲声明式 API 的最后一块拼图——CsgNode 树的 JSON 序列化:如何把整棵建模树存成 JSON、再读回来,实现建模参数的持久化与传输。