znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第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,意味着它拥有 IdName,可以被 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:参数化轮廓的核心

ParametricProfileWProfileHSSProfileLProfileCProfile 等型钢的基类。它的核心设计是:型钢截面顶点用表达式描述,运行时通过 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 字段(如 dbftwtfkdes 等)在 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() 被调用时,实体被三角剖分成 MeshMesh 再被序列化为 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,平面剖切

下一章将把这些几何基础应用到实际的建筑元素——墙、梁、柱、楼板——中。


← 上一章 目录 下一章 →