第04章:几何系统(下)——轮廓、变换与实体
第03章介绍了向量、曲线、多边形——它们是二维几何的基本构件。本章将这些基本构件组合起来,进入三维几何的核心:轮廓将多边形组织为带空洞的剖面,型钢剖面为结构工程提供标准化截面,变换将二维轮廓提升到三维空间,网格是最终的三角化结果,实体在 BREP(边界表示)层面维护精确的三维拓扑。
4.1 Profile:轮廓系统
Profile 是 Elements 二维剖面的核心抽象。它最简洁的定义是:一个外边界多边形(Perimeter)+ 零个或多个孔洞多边形(Voids)。一切拉伸、扫描操作都从 Profile 出发。
namespace Elements.Geometry
{
public class Profile : Element, IEquatable<Profile>
{
public Polygon Perimeter { get; set; } // 外边界
public IList<Polygon> Voids { get; set; } // 孔洞列表
}
}
Profile 继承自
Element,意味着它拥有Id和Name,可以被Model独立管理。这在”型钢截面表”场景中很有价值——每个型号的截面都是一个可引用的Profile元素。
4.1.1 创建轮廓
最基础的用法:用矩形多边形创建剖面。
// 实心矩形剖面
var solidProfile = new Profile(Polygon.Rectangle(0.2, 3.0));
// 带圆形孔洞的剖面(如圆管截面)
var outerCircle = new Circle(Vector3.Origin, 0.15).ToPolygon(64);
var innerCircle = new Circle(Vector3.Origin, 0.10).ToPolygon(64);
var tubeProfile = new Profile(outerCircle, new[] { innerCircle });
// 多个同心圆的管道剖面(外径 0.2,内径 0.18)
var pipeOuter = new Circle(Vector3.Origin, 0.20).ToPolygon(64);
var pipeInner = new Circle(Vector3.Origin, 0.18).ToPolygon(64);
var pipeProfile = new Profile(pipeOuter, pipeInner);
在使用 Profile(Polygon, IList<Polygon>) 构造函数时,孔洞多边形必须完全位于外边界内部,否则布尔运算会失败。Elements 内部使用 Clipper 库处理多边形裁剪——先用外边界减去所有孔洞,得到实际的有效面积。
Profile 还提供了从多边形列表自动推断外边界和孔洞的构造函数:
// 传入多个多边形,第一个(不被其他包含的)为外边界,其余为孔洞
var polygons = new List<Polygon> { outer, inner1, inner2 };
var profile = new Profile(polygons); // 自动判断层次关系
4.1.2 OrientVoids:孔洞方向校正
多边形有顺时针(CW)和逆时针(CCW)两种环绕方向。在 Elements 中,外边界(Perimeter)应为 CCW,孔洞(Voids)应为 CW——这是 Clipper 库的约定。如果构造后直接修改了 Voids 列表,需要手动调用 OrientVoids() 校正:
profile.Voids = new List<Polygon> { somePolygon };
profile.OrientVoids(); // 确保 void 的环绕方向与外边界相反
4.1.3 EdgeThickness:边厚度体系
Profile 支持为每条边单独设置内侧厚度和外侧厚度。这对建筑的表皮、幕墙构件非常实用——外轮廓是玻璃的外边界,内侧厚度表示龙骨系统,外侧厚度表示密封胶条。
// 为整个轮廓设置统一的边厚度
profile.SetEdgeThickness(innerWidth: 0.05, outerWidth: 0.02);
// 为每条边单独设置(按顶点顺序对应)
profile.SetEdgeThickness(
(0.05, 0.02), // 边1: 内厚0.05, 外厚0.02
(0.05, 0.02), // 边2
(0.08, 0.01), // 边3: 更厚的内侧
(0.05, 0.02) // 边4
);
// 获取当前厚度信息
var thicknesses = profile.GetEdgeThickness();
// 返回 List<(double inner, double outer)>
设置好边厚度后,可以使用两个关键的变换方法:
// 获取内侧轮廓:将外边界向内偏移,减去边厚度
var interior = profile.ThickenedInteriorProfile();
// 获取外侧轮廓:将外边界向外偏移
var exterior = profile.ThickenedExteriorProfile();
// 获取增厚后的边多边形(用于生成龙骨/框架几何体)
var thickenedEdges = profile.ThickenedEdgePolygons();
工作流程示例:幕墙竖梃截面
// 幕墙玻璃外边界
var glassProfile = new Profile(Polygon.Rectangle(1.2, 3.0));
glassProfile.SetEdgeThickness(
(0.08, 0.0), // 竖梃内侧 80mm
(0.08, 0.0),
(0.06, 0.0), // 横框内侧 60mm
(0.06, 0.0)
);
// 竖梃/横框的内轮廓(等同于玻璃的可见区域)
var glassInterior = glassProfile.ThickenedInteriorProfile();
// 竖梃/横框的实体多边形(用于3D建模)
var framePolygons = glassProfile.ThickenedEdgePolygons();
4.1.4 轮廓的变换与操作
Profile 提供了丰富的几何操作方法,底层由 Clipper 库驱动。
变换与复制:
// 创建变换后的副本
var moved = profile.Transformed(new Transform(5, 0, 0));
// 原地变换(修改自身)
profile.Transform(new Transform(0, 0, 0, rotation: 45));
// 缩放
var scaled = profile.Scale(1.5);
// 翻转
var reversed = profile.Reversed();
布尔运算:
// 并集
var union = profileA.Union(profileB, tolerance: Vector3.EPSILON);
// 差集
var differences = Profile.Difference(setA, setB);
// 交集
var intersections = Profile.Intersection(setA, setB);
// 多个轮廓的并集
var allUnion = Profile.UnionAll(new[] { p1, p2, p3 });
偏移与分割:
// 向外偏移 0.1 米
var offsetProfiles = profile.Offset(0.1);
// 用一条折线分割轮廓
var splitProfiles = Profile.Split(
new[] { profile },
new Polyline(new[] { new Vector3(0, -1, 0), new Vector3(0, 1, 0) })
);
从多边形集合创建:
// 从一堆多边形中自动生成 Profile(识别内外包含关系)
var profiles = Profile.CreateFromPolygons(manyPolygons);
4.1.5 投影与包含判断
// 投影到指定平面
var projected = profile.Project(plane);
// 判断点是否在轮廓内
bool inside = profile.Contains(new Vector3(0.5, 0.5, 0));
// 获取点相对于轮廓的详细信息
profile.Contains(point, out Containment containment);
// containment: Inside / Outside / OnEdge / OnVertex
4.1.6 轮廓的面积计算
面积计算是挖除孔洞后的净面积,使用 Clipper 库的 ctDifference 实现:
double area = profile.Area();
内部实现:将外边界和所有孔洞送入 Clipper 做差值运算,对各部分的面积求和后再除以 EPSILON 的平方(Clipper 内部用整数坐标,需要比例转换)。
4.2 型钢剖面体系
Elements 在 Elements.Geometry.Profiles 命名空间下提供了完整的型钢剖面类型体系,覆盖美标(AISC)结构钢的主要截面形式。
4.2.1 继承架构
Profile (Element)
├── ParametricProfile ─── 参数化轮廓,使用 Roslyn 脚本评估顶点表达式
│ ├── WProfile ─── W 型宽翼缘工字钢
│ ├── HSSProfile ─── 矩形中空结构型钢(带孔洞)
│ ├── STProfile ─── 结构 T 型钢
│ ├── WTProfile ─── 宽翼缘 T 型钢
│ ├── CProfile ─── 槽钢(C 型钢)
│ ├── MCProfile ─── 多用途槽钢
│ └── LProfile ─── 角钢(L 型钢)
├── RHSProfile ─── 矩形中空型钢(直接多边形构造)
│ └── SHSProfile ─── 方形中空型钢
└── WideFlangeProfile ─── 宽翼缘型钢(直接多边形构造,带对中选项)
4.2.2 各型钢对应关系
| 类名 | 中文名称 | 钢结构件 | 截面示意图 |
|---|---|---|---|
WProfile |
W 型宽翼缘工字钢 | 钢梁、钢柱(美标 W-shape) | 工字形,上下翼缘宽 |
HSSProfile |
矩形中空结构型钢 | 方钢管柱、桁架构件 | 矩形,中空 |
RHSProfile |
矩形中空型钢 | 矩形管构件 | 矩形,中空(直接构造) |
SHSProfile |
方形中空型钢 | 方管柱、桁架弦杆 | 正方形,中空 |
LProfile |
角钢 | 连接件、斜撑、桁架腹杆 | L 形 |
CProfile |
槽钢 | 檩条、墙梁、次梁 | C 形 |
MCProfile |
多用途槽钢 | 特殊的槽钢截面 | C 形(与 C 形类似但尺寸不同) |
STProfile |
结构 T 型钢 | 桁架弦杆 | T 形,翼缘较窄 |
WTProfile |
宽翼缘 T 型钢 | 桁架构件(从 W 型切割) | T 形,翼缘较宽 |
WideFlangeProfile |
宽翼缘型钢 | 通用钢梁/钢柱 | 工字形(非参数化) |
4.2.3 ParametricProfile:参数化轮廓的核心
ParametricProfile 是 WProfile、HSSProfile、LProfile、CProfile 等型钢的基类。它的核心设计是:型钢截面顶点用表达式描述,运行时通过 Roslyn C# 脚本引擎评估表达式生成实际坐标。
public class ParametricProfile : Profile
{
// 外边界顶点表达式列表
public List<VectorExpression> PerimeterVectorExpressions { get; }
// 孔洞顶点表达式列表(二维,每个内层列表是一个孔洞)
public List<List<VectorExpression>> VoidVectorExpressions { get; }
// 异步评估表达式,生成实际多边形
public async Task SetGeometryAsync()
{
Perimeter = await CreatePolygonFromScriptAsync(_perimeterScript);
// ... 同样处理孔洞
}
}
VectorExpression 是表达式的字符串描述:
// VectorExpression 示例
new VectorExpression("bf/2", "d/2") // X = bf/2, Y = d/2
以 WProfile 为例,它的 12 个顶点表达式定义了工字形截面的轮廓:
顶点 0: ( bf/2, d/2) 右上翼缘外缘
顶点 1: (-bf/2, d/2) 左上翼缘外缘
顶点 2: (-bf/2, d/2 - tf) 左上翼缘内侧
顶点 3: (-tw/2, d/2 - tf) 左上腹板
顶点 4: (-tw/2, -d/2 + tf) 左下腹板
顶点 5: (-bf/2, -d/2 + tf) 左下翼缘内侧
顶点 6: (-bf/2, -d/2) 左下翼缘外缘
顶点 7: ( bf/2, -d/2) 右下翼缘外缘
顶点 8: ( bf/2, -d/2 + tf) 右下翼缘内侧
顶点 9: ( tw/2, -d/2 + tf) 右下腹板
顶点10: ( tw/2, d/2 - tf) 右上腹板
顶点11: ( bf/2, d/2 - tf) 右上翼缘内侧
型钢类上的 double 字段(如 d、bf、tw、tf、kdes 等)在 SetPropertiesFromProfileData() 中被 CSV 数据填充,然后在 SetGeometryAsync() 中通过脚本评估生成精确的坐标。
HSSProfile 的特殊之处:除了外边界(外围矩形),还定义了孔洞(内围矩形),从而自然表达了中空结构型钢的壁厚。
// HSSProfile 的外边界 + 孔洞
perimeter: 4个顶点(B×Ht 外矩形)
voids: 4个顶点((B-2*tnom)×(Ht-2*tnom) 内矩形)
4.2.4 WideFlangeProfile:带对齐的宽翼缘型钢
WideFlangeProfile 是另一种宽翼缘型钢实现,直接使用多边形计算而非参数化表达式。它的优势是支持水平和垂直对齐:
var wfProfile = new WideFlangeProfile(
name: "W12x26",
id: Guid.NewGuid(),
bf: 0.165, // 翼缘宽度 (m)
d: 0.310, // 截面高度 (m)
tf: 0.0097, // 翼缘厚度 (m)
tw: 0.0058, // 腹板厚度 (m)
verticalAlignment: VerticalAlignment.Center, // 垂直居中
horizontalAlignment: HorizontalAlignment.Center // 水平居中
);
对齐选项:
VerticalAlignment.Top / Center / Bottom:顶部/居中/底部对齐HorizontalAlignment.Left / Center / Right:左/居中/右对齐
这在结构建模中非常重要——梁顶对齐于楼面标高、柱偏心于轴线等场景都需要这些对齐参数。
4.3 ProfileData 与工厂模式
Elements 的型钢截面数据来自 CSV 文件,通过工厂模式按需创建 Profile 实例。
4.3.1 工厂架构
ProfileFactoryBase<TProfileType, TProfile> (抽象基类,字符串数据源)
├── WideFlangeProfileFactory (WideFlangeProfile 工厂)
├── RHSProfileFactory / SHSProfileFactory (RHS/SHS 工厂)
│
ParametricProfileFactory<TProfileType, TProfile> (参数化工厂,文件路径数据源)
├── WProfileFactory → WProfileType (枚举) → WProfile
├── HSSProfileFactory → HSSProfileType (枚举) → HSSProfile
├── LProfileFactory → LProfileType (枚举) → LProfile
├── CProfileFactory → CProfileType (枚举) → CProfile
├── MCProfileFactory → MCProfileType (枚举) → MCProfile
├── STProfileFactory → STProfileType (枚举) → STProfile
└── WTProfileFactory → WTProfileType (枚举) → WTProfile
4.3.2 ProfileFactoryBase:字符串数据源
public abstract class ProfileFactoryBase<TProfileType, TProfile>
{
protected List<string[]> _profileData = new List<string[]>();
public ProfileFactoryBase(string data)
{
// 从字符串读取 CSV(跳过第一行标题),每行存入 _profileData
}
public IEnumerable<TProfile> AllProfiles() { ... }
public abstract TProfile GetProfileByName(string name);
public abstract TProfile GetProfileByType(TProfileType type);
}
ProfileFactoryBase 接收嵌入的 CSV 字符串数据——这意味完整的 AISC 型钢截面表被编译进了 DLL。
4.3.3 ParametricProfileFactory:文件路径数据源
ParametricProfileFactory 从外部 CSV 文件读取截面数据:
public class ParametricProfileFactory<TProfileType, TProfile>
where TProfileType : Enum
where TProfile : ParametricProfile
{
private readonly Dictionary<string, Dictionary<string, double>> _profileData;
private readonly Dictionary<TProfileType, TProfile> _profileCache;
public ParametricProfileFactory(string csvPath, double conversion)
{
// 1. 查找 CSV 文件(绝对路径 → 相对工作目录 → 相对程序集目录)
// 2. 逐行解析,第一行为列名(keys),后续为数据行
// 3. 对所有数值应用 conversion 因子
}
}
关键实现细节:
// 英寸 → 米的单位转换
double.TryParse(values[i], out double value);
currentProfileData.Add(key, value * conversion);
// 例如 conversion = 0.0254(英寸转米)
4.3.4 创建型钢截面实例的完整流程
// 1. 创建工厂,指定 CSV 路径和单位转换因子
var wFactory = new WProfileFactory(
"ProfileData/WProfile.csv", // CSV 文件路径
0.0254 // 英寸→米转换
);
// 2. 按名称异步获取(名称对应枚举值,如 "W12X26")
var w12x26 = await wFactory.GetProfileByNameAsync("W12X26");
// 3. 按枚举类型获取
var profile = await wFactory.GetProfileByTypeAsync(WProfileType.W12X26);
// 4. 获取所有截面
var allProfiles = await wFactory.AllProfilesAsync();
工厂内部的创建流程:
GetProfileByNameAsync("W12X26")
├── Enum.Parse<WProfileType>("W12X26") → WProfileType.W12X26
├── 检查 _profileCache(已缓存则直接返回)
├── Activator.CreateInstance<WProfile>() → 创建实例
├── SetPropertiesFromProfileData(profileData)
│ └── 反射设置 d, bf, tw, tf, kdes 等字段值
├── await SetGeometryAsync()
│ └── Roslyn 脚本评估 VectorExpression → 生成 Polygon
└── 存入 _profileCache
4.3.5 CSV 数据格式示例
AISC 型钢截面 CSV 的典型结构:
W,d,bf,tw,tf,kdes,kdet,k1,bf_2tf,h_tw,Ix,Zx,Sx,rx,Iy,Zy,Sy,ry,J,Cw,...
W44X335,44.000,15.900,1.030,1.770,2.520,1.770,1.312,6.68,21.20,16200,748,812,19.00,1200,151,76.7,3.74,64.7,107000,...
W40X655,43.600,16.800,1.970,3.540,5.040,2.540,2.250,3.86,13.10,25600,1200,1280,19.70,5620,675,351,9.29,267,...
- 列名与
WProfile类上的public double字段精确对应 - 数值以英寸为单位,工厂中通过
conversion因子转为米 - 名称中不能包含
-字符——工厂内部会用__替换(因为-不能作为枚举名)
4.4 Transform:变换系统
Transform 是 Elements 中所有对象在三维空间中定位、旋转和缩放的统一机制。
4.4.1 Matrix:3×4 列主序矩阵
Transform 的核心是一个 Matrix——列主序(column-ordered)的 3×4 矩阵。前 3 列表示坐标系的 X/Y/Z 轴,第 4 列表示平移(原点位置)。
public partial class Matrix
{
// 12 个分量:m11-m33(旋转/缩放)+ tx/ty/tz(平移)
public double[] Components { get; set; } = new double[12];
// 矩阵布局(列主序):
// [m11 m12 m13 tx] ← X轴分量(m11,m12,m13), 平移X(tx)
// [m21 m22 m23 ty] ← Y轴分量
// [m31 m32 m33 tz] ← Z轴分量
// 轴向量
public Vector3 XAxis => new Vector3(m11, m12, m13);
public Vector3 YAxis => new Vector3(m21, m22, m23);
public Vector3 ZAxis => new Vector3(m31, m32, m33);
public Vector3 Translation => new Vector3(tx, ty, tz);
}
向量变换公式:
p' = p × M
即:
x' = x*m11 + y*m21 + z*m31 + tx
y' = x*m12 + y*m22 + z*m32 + ty
z' = x*m13 + y*m23 + z*m33 + tz
4.4.2 创建变换
// 单位变换(无平移、无旋转、无缩放)
var identity = new Transform();
// 纯平移
var translate = new Transform(5, 2, 3); // 位移到 (5, 2, 3)
var translate2 = new Transform(new Vector3(5, 2, 3));
// 平移 + 绕 Z 轴旋转
var moveAndRotate = new Transform(5, 2, 3, rotation: 45);
// 由原点 + Z 轴方向定义
var orientZ = new Transform(new Vector3(3, 0, 0), new Vector3(0, 1, 0));
// 由原点 + X 轴 + Z 轴定义
var custom = new Transform(
new Vector3(10, 0, 0), // 原点
new Vector3(1, 0, 0), // X 轴方向
new Vector3(0, 0, 1) // Z 轴方向
);
// 由平面定义
var fromPlane = new Transform(new Plane(origin, normal));
4.4.3 变换操作
var t = new Transform();
// 平移
t.Move(new Vector3(5, 0, 0));
t.Move(5, 0, 0); // 等效写法
var moved = t.Moved(5, 0, 0); // 返回副本
// 旋转
t.Rotate(Vector3.ZAxis, 45); // 绕 Z 轴旋转 45 度
t.Rotate(45); // 绕 Z 轴旋转(默认轴)
t.RotateAboutPoint(center, axis, angle); // 绕指定点旋转
var rotated = t.Rotated(Vector3.ZAxis, 90);
// 缩放
t.Scale(2.0); // 均匀缩放 2 倍
t.Scale(new Vector3(2, 1, 3)); // 非均匀缩放
t.Scale(2.0, centerPoint); // 绕指定点均匀缩放
var scaled = t.Scaled(1.5);
// 镜像(关于法线为 n 的平面反射)
t.Reflect(new Vector3(1, 0, 0));
// 逆矩阵
t.Invert(); // 原地求逆
var inv = t.Inverted(); // 返回逆矩阵副本
// 级联(矩阵乘法)
t.Concatenate(otherTransform); // t = t * other
var combined = t.Concatenated(other); // 返回副本
4.4.4 应用变换到几何对象
var t = new Transform(new Vector3(10, 0, 5), 30);
// 变换点(包含平移)
var newPoint = t.OfPoint(originalPoint);
// 变换向量(忽略平移,仅旋转/缩放)
var newVector = t.OfVector(originalVector);
// 变换其他几何类型
var newProfile = t.OfProfile(profile); // 变换 Profile
var newPlane = t.OfPlane(plane); // 变换平面
var newPolygons = t.OfPolygons(polygons); // 变换多边形数组
推荐使用几何对象自身的 Transformed() 方法,Transform.Of* 系列方法已标记为 [Obsolete]:
// 推荐的写法
var movedProfile = profile.Transformed(t);
var movedCurve = curve.Transformed(t);
var movedPolygon = polygon.TransformedPolygon(t);
var movedLine = line.TransformedLine(t);
4.4.5 Transform 的链式操作
// 先平移到 (10, 0, 0),再绕 Z 轴旋转 45°,最后缩放 2 倍
var result = new Transform()
.Moved(10, 0, 0)
.Rotated(Vector3.ZAxis, 45)
.Scaled(2.0);
4.4.6 获取变换的坐标系平面
var t = new Transform(new Vector3(5, 3, 0));
var xyPlane = t.XY(); // Z = t.ZAxis, 原点 = t.Origin 的平面
var xzPlane = t.XZ(); // Z = t.YAxis 的平面
var yzPlane = t.YZ(); // Z = t.XAxis 的平面
4.5 Mesh:三角网格
Mesh 是 Elements 的三角形网格表示,是 BREP 实体到渲染引擎的桥梁。当 Solid.ToMesh() 被调用时,实体被三角剖分成 Mesh,Mesh 再被序列化为 GraphicsBuffers,最终进入 glTF/GLB 导出管线。
4.5.1 基本结构
public partial class Mesh
{
public IList<Vertex> Vertices { get; set; } // 顶点列表
public IList<Triangle> Triangles { get; set; } // 三角形列表
}
public class Vertex
{
public Vector3 Position { get; set; } // 顶点坐标
public Vector3 Normal { get; set; } // 法线
public UV UV { get; set; } // 纹理坐标
public Color Color { get; set; } // 顶点颜色
public int Index { get; internal set; } // 在 Vertices 列表中的索引
public List<Triangle> Triangles { get; }// 关联的三角形(入射面)
}
public class Triangle
{
public List<Vertex> Vertices; // 3 个顶点
public Vector3 Normal; // 面法线
}
Vertex.Triangles 是反向索引——每个顶点知道哪些三角形包含它。这是在三角剖分后自动维护的,用于法线计算和拓扑查询。
4.5.2 手动构建 Mesh
var mesh = new Mesh();
// 添加顶点
var v0 = mesh.AddVertex(new Vector3(0, 0, 0));
var v1 = mesh.AddVertex(new Vector3(1, 0, 0));
var v2 = mesh.AddVertex(new Vector3(0, 1, 0));
// 添加三角形
mesh.AddTriangle(v0, v1, v2);
// 也可以直接添加 Triangle 对象
var t = new Triangle(v0, v1, v2);
mesh.AddTriangle(t);
// 计算顶点法线(面法线求平均)
mesh.ComputeNormals();
4.5.3 顶点合并
// 合并模式下,搜索半径内的已有顶点会被复用
var v = mesh.AddVertex(
position: new Vector3(1, 0, 0),
merge: true,
edgeAngle: 30.0 // 如果法线夹角超过此值,不合并
);
内部使用 PointOctree<Vertex> 进行空间索引,避免 O(n) 搜索。
4.5.4 从 Solid 到 Mesh 的三角剖分流程
Elements 的三角剖分经过以下步骤:
Csg.Solid(CSG 布尔运算结果)
│
├── SolidTessellationTargetProvider
│ 将 Solid 的每个 Face 作为独立的剖分目标
│
├── LibTessDotNet (Tess)
│ 对每个 Face 的外边界和孔洞进行 Delaunay 三角剖分
│ 适配器: SolidFaceTessAdapter / CsgPolygonTessAdapter
│
├── Tess.ToMesh(transform, color)
│ 将 LibTessDotNet 的输出转为 Elements.Mesh
│ 第一个三角形用于计算 UV 的基向量 (U, V)
│
└── mesh.AddMesh(faceMesh)
将各个面的剖分结果合并为完整 Mesh
UV 坐标计算:剖分时,取第一个三角形的三条边计算基向量(U, V),然后对所有顶点做点积映射。这对平面面片效果很好,对曲面则有失真(目前 Elements 不直接处理非平面面片)。
4.5.5 GraphicsBuffers:渲染缓冲区
var mesh = solid.ToMesh(); // Solid → Mesh
var buffers = mesh.GetBuffers(); // Mesh → GraphicsBuffers
// GraphicsBuffers 包含:
// - 顶点位置数组
// - 法线数组
// - UV 坐标数组
// - 颜色数组
// - 索引数组 (ushort[])
这是 glTF 序列化的最终数据格式。
4.5.6 STL 导入
// 从 STL 文件加载(默认单位为毫米)
var mesh = Mesh.FromSTL("part.stl", LengthUnit.Millimeter);
// 手动指定单位转换
var meshInMeters = Mesh.FromSTL("part.stl", LengthUnit.Inch);
4.5.7 网格查询
// 体积(开放网格不精确)
double volume = mesh.Volume();
// 开放边(只属于一个三角形的边)
var nakedEdges = mesh.GetNakedEdges(); // List<Line>
// 开放边界折线(用于检测网格是否水密)
var boundaries = mesh.GetNakedBoundaries(); // List<Polyline>
// 射线相交测试(使用八叉树加速)
Ray ray = new Ray(origin, direction);
if (mesh.Intersects(ray, out Vector3 hitPoint))
{
Console.WriteLine($"命中点: {hitPoint}");
}
4.6 Solid:半边结构实体
Solid 是 Elements 几何内核中最底层的精确三维表示——一个基于半边数据结构(Half-Edge Data Structure)的边界表示(BREP)。
4.6.1 数据结构
public partial class Solid
{
// 三个核心字典:面、边、顶点,均以 uint ID 索引
public Dictionary<long, Face> Faces { get; }
public Dictionary<long, Edge> Edges { get; }
public Dictionary<long, Vertex> Vertices { get; }
}
半边数据结构的关键元素:
Face (面)
├── Loop Outer 外环(CCW 方向)
└── Loop[] Inner 内环/孔洞环(CW 方向)
Edge (无向边)
├── HalfEdge Left 左半边
└── HalfEdge Right 右半边
HalfEdge (有向半边)
├── Vertex Vertex 起点顶点
├── HalfEdge Next 环中的下一个半边
├── HalfEdge Prev 环中的前一个半边
├── Loop Loop 所属的环
└── Edge Edge 所属的无向边
Loop (环)
└── HalfEdge[] Edges 半边序列(按环的顺序)
Vertex
└── Vector3 Point 三维坐标
半边结构的关键约定:
- 对于 Face 的外环(Outer),从外部观察时半边为逆时针(CCW)方向
- 对于 Face 的内环/孔洞(Inner),半边为顺时针(CW)方向
- Edge 的
Left半边方向对应 Face 的外环遍历方向 - Edge 的
Right半边方向与Left相反,对应相邻 Face 的外环遍历方向
这构成了一个完整的二维流形(2-Manifold)拓扑结构——每条边恰好属于两个面(边界边是特殊情况)。
4.6.2 静态工厂方法
一般不直接 new Solid(),而是通过静态工厂方法创建:
CreateLamina:层板实体
// 从多边形创建薄片实体(两面体,零厚度)
var lamina = Solid.CreateLamina(
new Polygon(new[] {
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(1, 1, 0),
new Vector3(0, 1, 0)
})
);
// 带孔洞的层板
var laminaWithHole = Solid.CreateLamina(
outerPolygon,
new[] { innerPolygon }
);
// 从 Profile 创建层板
var laminaFromProfile = Solid.CreateLamina(profile);
层板实体包含两个面(正面和背面),正反面的外环多边形方向相反。
SweepFace:沿方向拉伸
// 沿 Z 轴正方向拉伸
var solid = Solid.SweepFace(
perimeter: outerPolygon,
holes: new[] { innerPolygon }, // 可选孔洞
direction: Vector3.ZAxis,
distance: 3.0,
bothSides: false, // 是否双向拉伸
rotation: 0.0 // 绕拉伸方向的旋转角度
);
// 双向拉伸(起点居中)
var solidBoth = Solid.SweepFace(
perimeter,
holes: null,
new Vector3(0, 0, 1),
6.0,
bothSides: true
);
SweepFaceAlongCurve:沿曲线扫描
// 沿弧线扫描
var arc = new Arc(Vector3.Origin, 3.0, 0, Math.PI / 2);
var solid = Solid.SweepFaceAlongCurve(
perimeter: crossSection,
holes: null,
curve: arc,
startSetback: 0.1, // 起始退距
endSetback: 0.1, // 终止退距
profileRotation: 0
);
这个方法的内部逻辑比简单的 SweepFace 更复杂。当曲线是 Polygon 时,在两个相邻平面之间投影和连接;当是 Bezier 时,逐段扫掠并添加起止端盖;对于其他曲线类型,借助 Edge[] 的增量扫掠实现。
4.6.3 手动构建 Solid(低层 API)
var solid = new Solid();
// 添加顶点
var v0 = solid.AddVertex(new Vector3(0, 0, 0));
var v1 = solid.AddVertex(new Vector3(1, 0, 0));
var v2 = solid.AddVertex(new Vector3(1, 1, 0));
var v3 = solid.AddVertex(new Vector3(0, 1, 0));
// 添加边(连接两个顶点)
var e0 = solid.AddEdge(v0, v1);
var e1 = solid.AddEdge(v1, v2);
var e2 = solid.AddEdge(v2, v3);
var e3 = solid.AddEdge(v3, v0);
// 构建环(从半边序列)
var loop = new Loop(new[] { e0.Left, e1.Left, e2.Left, e3.Left });
// 添加面(环作为外环)
var face = solid.AddFace(loop);
更常见的是通过 AddFace(Polygon) 一次性完成:
var solid = new Solid();
var face1 = solid.AddFace(
bottomPolygon,
inner: null,
mergeVerticesAndEdges: false, // 不合并顶点(新建独立顶点)
transform: null,
reverse: false
);
4.6.4 BREP 查询
// 统计信息
Console.WriteLine($"Faces: {solid.Faces.Count}, Edges: {solid.Edges.Count}, Vertices: {solid.Vertices.Count}");
// 遍历所有面
foreach (var face in solid.Faces.Values)
{
var plane = face.Plane();
Console.WriteLine($"Face {face.Id}: outer edges={face.Outer.Edges.Length}, inner loops={face.Inner?.Length ?? 0}");
}
4.6.5 平面剖切
// 用平面对实体进行剖切
Plane slicePlane = new Plane(Vector3.Origin, Vector3.YAxis);
if (solid.Intersects(slicePlane, out List<Polygon> result))
{
foreach (var poly in result)
{
Console.WriteLine($"剖面多边形: {poly.Vertices.Count} 个顶点");
}
}
Intersects 方法遍历所有面,找出与平面相交的边,收集交点,构建半边图(HalfEdgeGraph2d),然后通过 Polygonize() 生成封闭多边形。
4.6.6 从 Solid 生成 Mesh
// Solid → Mesh(三角剖分)
var mesh = solid.ToMesh();
// 或者直接到 GraphicsBuffers
var solidRepresentation = new SolidRepresentation(solid);
var buffers = solidRepresentation.ToGraphicsBuffers();
4.6.7 Solid 在 CSG 管道中的角色
在 Elements 的完整几何管道中,Solid 处于中间位置:
Profile + Extrude/Sweep 操作
│
▼
Solid (BREP,半边结构)
│
├── ToCsg() ──→ Csg.Solid
│ │
│ ├── CSG Union/Subtract/Intersect
│ │ (自定义 fork 的 Csg 库)
│ │
│ ├── 三角剖分 (LibTessDotNet)
│ │
│ └── Elements.Geometry.Mesh
│
└── ToMesh() ──→ Elements.Geometry.Mesh
Solid.ToCsg() 是关键转换方法:遍历每个 Face,将其外环和内环送入 LibTessDotNet 做二维三角剖分,然后将小三角面片转换为 Csg.Polygon 列表,最后组合成 Csg.Solid 用于布尔运算。
在布尔运算完成后(如 ConstructedSolid 中),生成的 Csg.Solid 又被三角剖分为 Mesh,完成了 BREP → CSG → Mesh 的完整流水线。
4.7 小结
本章覆盖了 Elements 几何系统的后半部分——从二维轮廓到三维实体的完整路径:
| 概念 | 核心类型 | 关键能力 |
|---|---|---|
| 轮廓 | Profile |
Perimeter + Voids,带边厚度,布尔运算,偏移,分割 |
| 型钢剖面 | ParametricProfile 子类 |
Roslyn 脚本评估,CSV 数据驱动,AISC 全截面覆盖 |
| 工厂模式 | ProfileFactoryBase / ParametricProfileFactory |
CSV 解析,英寸→米转换,缓存,枚举映射 |
| 变换 | Transform / Matrix |
3×4 列主序矩阵,平移/旋转/缩放/镜像/逆矩阵/级联 |
| 网格 | Mesh / Vertex / Triangle |
三角形网格,法线计算,八叉树顶点合并,UV 坐标 |
| 实体 | Solid (BREP) |
半边结构,Faces/Edges/Vertices,CreateLamina/SweepFace,平面剖切 |
下一章将把这些几何基础应用到实际的建筑元素——墙、梁、柱、楼板——中。