第03章:几何系统(上)——向量、曲线与多边形
Elements 的几何系统是整个库的核心支柱。所有建筑元素——墙、梁、柱、楼板——最终都需要精确的几何描述才能在三维空间中存在。Elements 没有依赖第三方几何内核(如 OpenCASCADE 或 Parasolid),而是从零构建了一个完整的、适合 AEC 领域的计算几何库。这个几何库涵盖从基础向量运算到复杂多边形布尔运算的全套能力。
本章聚焦于几何系统的基础层:精度约定、向量系统、曲线类层次结构,以及多边形体系。下一章(第04章)将继续深入到轮廓变换与实体建模。
3.1 右手坐标系与精度约定
3.1.1 右手坐标系
Elements 采用标准的右手坐标系:+X 向右,+Y 向前,+Z 向上。这与大多数 BIM 工具(Revit、Rhino)的默认坐标系一致。在坐标系确立之后,所有旋转变换遵循右手定则:从旋转轴的正方向看向原点,逆时针为正向旋转。
// 右手定则验证:X 叉乘 Y 必须等于 Z
var x = Vector3.XAxis; // (1, 0, 0)
var y = Vector3.YAxis; // (0, 1, 0)
var z = x.Cross(y); // (0, 0, 1) == ZAxis
Console.WriteLine(z.IsAlmostEqualTo(Vector3.ZAxis)); // True
3.1.2 浮点精度常量 Vector3.EPSILON
所有几何计算的核心容差由 Vector3.EPSILON 定义,值为 1e-5(0.00001)。这个值的选择经过了权衡:足够大以吸收大多数浮点累积误差,又足够小以保证建筑尺度的精度(毫米级)。
// Vector3.EPSILON 的定义(源码:Vector3.cs:17)
public const double EPSILON = 1e-5;
此外还有角度比较专用的容差 COS_ANGLE_EPSILON,对应 0.001° 的余弦值(约 0.99999999984769128),用于快速判断两条方向向量是否平行:
// 角度容差(源码:Vector3.cs:22)
public const double COS_ANGLE_EPSILON = 0.99999999984769128;
3.1.3 IsAlmostEqualTo 容差比较方法
Elements 中几乎所有的”相等”判断都使用容差比较而非精确比较。核心方法是 Vector3.IsAlmostEqualTo,它通过欧氏距离的平方来判断两点是否在容差范围内:
// 源码:Vector3.cs:763-772
public bool IsAlmostEqualTo(Vector3 v, double tolerance = Vector3.EPSILON)
{
if ((this.X - v.X) * (this.X - v.X)
+ (this.Y - v.Y) * (this.Y - v.Y)
+ (this.Z - v.Z) * (this.Z - v.Z) < (tolerance * tolerance))
{
return true;
}
return false;
}
注意这里使用的是 tolerance * tolerance 作为距离平方的阈值,避免了开方运算,提高了性能。
除了 Vector3.IsAlmostEqualTo,还有一个配套的ApproximatelyEquals 扩展方法,用于比较两个浮点数:
// 双精度容差比较
double a = 1.0 / 3.0;
double b = 0.33333;
if (a.ApproximatelyEquals(b, 1e-4))
{
Console.WriteLine("近似相等");
}
3.1.4 跨平台浮点精度问题
不同平台(x86、x64、ARM)和不同 .NET 运行时对浮点运算的中间精度处理存在差异。x86 架构使用 80 位扩展精度寄存器,而 x64 使用 SSE 指令的 64 位精度。Elements 采用如下策略应对:
- 统一使用
double(64位浮点),不使用float,保证最小精度一致性 - 容差比较无处不在:从不使用
==直接比较浮点数,所有几何判断都通过EPSILON或ApproximatelyEquals进行 - 距离平方比较:尽量用平方距离代替开方后的距离,既提高性能也减少精度损失
- 构造时 NaN/Infinity 验证:
Vector3构造函数会检查 NaN 和 Infinity 值,防止无效数据进入系统
// 源码:Vector3.cs:40-51
public Vector3(double @x, double @y, double @z)
{
if (!Validator.DisableValidationOnConstruction)
{
if (Double.IsNaN(x) || Double.IsNaN(y) || Double.IsNaN(z))
throw new ArgumentOutOfRangeException("...One or more of the components was NaN.");
if (Double.IsInfinity(x) || Double.IsInfinity(y) || Double.IsInfinity(z))
throw new ArgumentOutOfRangeException("...One or more of the components was infinity.");
}
this.X = @x;
this.Y = @y;
this.Z = @z;
}
3.2 Vector3:三维向量的完整功能
Vector3 是 Elements 中使用频率最高的类型。它既表示三维空间中的点(位置),也表示方向向量。在 Elements 中,点和向量使用同一个类型,由使用上下文决定其语义。
3.2.1 基本结构
Vector3 是一个 struct(值类型),包含三个 double 分量 X、Y、Z。作为结构体,它避免了频繁分配堆内存,在大量几何运算中性能优势明显。
public partial struct Vector3 : IComparable<Vector3>, IEquatable<Vector3>
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
它实现了 IEquatable<Vector3>,并重载了 == 和 != 运算符,使得所有相等判断都走容差比较路径:
// 运算符重载直接调用 IsAlmostEqualTo
public static bool operator ==(Vector3 a, Vector3 b) => a.Equals(b);
public override bool Equals(object other)
{
if (!(other is Vector3)) return false;
return this.IsAlmostEqualTo((Vector3)other);
}
3.2.2 静态常量:坐标轴与原点
Elements 提供了一组全局静态常量来快速引用常用方向:
public static Vector3 XAxis => new Vector3(1, 0, 0); // +X 方向
public static Vector3 YAxis => new Vector3(0, 1, 0); // +Y 方向
public static Vector3 ZAxis => new Vector3(0, 0, 1); // +Z 方向
public static Vector3 Origin => new Vector3(); // 原点 (0,0,0)
public static Vector3 Min => new Vector3(double.MinValue, ...); // 最小值
public static Vector3 Max => new Vector3(double.MaxValue, ...); // 最大值
3.2.3 构造与隐式转换
Vector3 提供了多种构造方式,并支持从元组(tuple)隐式转换,极大提高了代码可读性:
// 标准构造函数
var a = new Vector3(10, 20, 30);
var b = new Vector3(5, 5); // Z 默认为 0
// 从元组隐式转换——Elements 独有的语法糖
Vector3 c = (10, 20, 30); // 三元素元组
Vector3 d = (5.5, 3.2); // 二元素元组,Z=0
Vector3 e = (1, 2, 3); // 三元素整数元组
// 复制构造
var f = new Vector3(a);
这种隐式转换使得使用 Polygon 或 Polyline 时非常方便:
var polyline = new Polyline((0,0,0), (10,0,0), (10,10,0), (0,10,0));
3.2.4 向量运算:加减乘除与运算符重载
Vector3 重载了所有基本算术运算符,使得向量运算代码自然直观:
Vector3 p1 = (10, 20, 5);
Vector3 p2 = (5, 10, 0);
Vector3 sum = p1 + p2; // (15, 30, 5)
Vector3 diff = p1 - p2; // (5, 10, 5)
Vector3 scaled = p1 * 3.0; // (30, 60, 15)
Vector3 halved = p1 / 2.0; // (5, 10, 2.5)
Vector3 negated = -p1; // (-10, -20, -5)
3.2.5 Dot(点积)与 Cross(叉积)
这两个方法是三维几何计算的基石:
Vector3 v1 = (1, 0, 0);
Vector3 v2 = (0, 1, 0);
double dot = v1.Dot(v2); // 0(垂直时为 0)
Vector3 cross = v1.Cross(v2); // (0, 0, 1)(右手定则:X × Y = Z)
// 点积判断方向关系
double projection = v1.Dot((0.707, 0.707, 0)); // ≈ 0.707
// 叉积有重载,可以直接传三个分量
Vector3 cross2 = v1.Cross(0, 1, 0); // 等同于 v1.Cross(v2)
3.2.6 长度、单位化与零向量判断
Vector3 v = (3, 4, 0);
double length = v.Length(); // 5(欧氏距离)
double lengthSq = v.LengthSquared(); // 25(避免开方,性能更好)
Vector3 unit = v.Unitized(); // (0.6, 0.8, 0),保持方向不变
bool isUnit = unit.IsUnitized(); // True(长度约等于 1)
// 零向量判断——所有分量均 < EPSILON
Vector3 zero = (0, 0, 0);
bool isZero = zero.IsZero(); // True
// 缩放
Vector3 doubled = v.Scale(2); // (6, 8, 0)
3.2.7 AngleTo:角度计算
AngleTo 返回两个向量之间的夹角(0° ~ 180°),单位为度:
Vector3 a = (1, 0, 0);
Vector3 b = (0, 1, 0);
double angle1 = a.AngleTo(b); // 90°
double angle2 = a.AngleTo((-1, 0, 0)); // 180°
// PlaneAngleTo:在指定平面内计算 0°~360° 的有向角度(逆时针)
double angle3 = a.PlaneAngleTo(b); // 90°(在 XY 平面内)
// 可以指定法向量为任意方向
double angle4 = a.PlaneAngleTo(b, Vector3.ZAxis); // 等同于上面
AngleTo 内部实现中处理了边界情况:当 dot 接近 1 时直接返回 0,避免 Acos(1) 的微小误差;当 dot 接近 -1 时返回 π。
3.2.8 DistanceTo:多种距离计算
DistanceTo 有多个重载,覆盖了点到点、点到线段、点到射线、点到平面、点到折线、点到多边形的各种距离计算需求:
Vector3 point = (5, 5, 0);
// 点到点的距离
double dist1 = point.DistanceTo(new Vector3(10, 10, 0)); // ≈ 7.07
// 点到线段的距离
var line = new Line((0, 0, 0), (10, 0, 0));
double dist2 = point.DistanceTo(line); // 5
// 点到线段的距离,同时获取最近点
double dist3 = point.DistanceTo(line, out Vector3 closest); // 5, closest=(5,0,0)
// 点到射线的距离
var ray = new Ray(Vector3.Origin, Vector3.XAxis);
double dist4 = point.DistanceTo(ray); // 5
// 点到平面的距离(有正负号区别)
var plane = new Plane(Vector3.Origin, Vector3.ZAxis);
double dist5 = point.DistanceTo(plane); // 0(点在 XY 平面上)
// 点到折线的最近距离
var polyline = new Polyline((0,0,0), (10,0,0), (10,10,0));
double dist6 = point.DistanceTo(polyline); // 找到最短的线段距离
// 点到多边形的距离(考虑内部点的情况)
var polygon = new Polygon((0,0,0), (10,0,0), (10,10,0), (0,10,0));
double dist7 = point.DistanceTo(polygon, out Vector3 closestOnPoly); // 点在内部时为投影距离
点到线段距离的内部实现(Vector3.cs:476-497)使用了投影参数 λ 的方法:将点到起点的向量投影到线段方向上,根据 λ < 0、0 ≤ λ ≤ 1、λ > 1 三种情况分别处理。
3.2.9 AtNEqualSpacesAlongLine:沿线段均匀采样
这是一个静态工具方法,沿给定线段创建 n 个等距采样点:
var line = new Line((0, 0, 0), (100, 0, 0));
// 沿线创建 4 个内部采样点,不含端点
var pts = Vector3.AtNEqualSpacesAlongLine(line, 4, includeEnds: false);
// pts: [(20,0,0), (40,0,0), (60,0,0), (80,0,0)]
// 包含端点
var ptsWithEnds = Vector3.AtNEqualSpacesAlongLine(line, 3, includeEnds: true);
// pts: [(0,0,0), (25,0,0), (50,0,0), (75,0,0), (100,0,0)]
源码实现直接通过步进长度值来计算各点位置:
// 源码:Vector3.cs:141-157
public static IList<Vector3> AtNEqualSpacesAlongLine(Line line, int n, bool includeEnds = false)
{
var l = line.Length();
var div = l / (double)(n + 1);
var pts = new List<Vector3>();
for (var t = 0.0; t <= l; t += div)
{
var pt = line.PointAt(t);
if ((t == 0.0 && !includeEnds) || (t == l && !includeEnds))
continue;
pts.Add(pt);
}
return pts;
}
3.2.10 其他实用方法
Vector3 a = (1, 2, 3);
Vector3 b = (4, 5, 6);
// 中点/均值
Vector3 mid = a.Average(b); // (2.5, 3.5, 4.5)
// 投影:将 a 投影到 b 上
Vector3 proj = a.ProjectOnto(b); // a 在 b 上的投影向量
// 投影到平面
Vector3 onPlane = a.Project(plane);
// 求反
Vector3 neg = a.Negate(); // (-1, -2, -3)
// 判断平行
bool parallel = Vector3.XAxis.IsParallelTo((2, 0, 0)); // True
// 判断 NaN
bool nan = new Vector3(double.NaN, 0, 0).IsNaN(); // True
// 转换为数组
double[] arr = a.ToArray(); // [1.0, 2.0, 3.0]
// 字符串表示(默认保留 4 位小数)
Console.WriteLine(a.ToString()); // "X:1.0000, Y:2.0000, Z:3.0000"
3.3 曲线类体系
Elements 的曲线系统采用双重抽象基类的分层架构:最顶层是 Curve(全部曲线的抽象),中间层是 BoundedCurve(有界曲线)和 TrimmedCurve<TBasis>(裁剪曲线),底层是具体曲线类型。
3.3.1 类层次结构总览
Curve (abstract) ← 顶层抽象基类
├── InfiniteLine ← 无限直线
├── Circle ← 圆(完整)
├── Ellipse ← 椭圆(完整)
├── BoundedCurve (abstract) ← 有界曲线
│ ├── Bezier ← 贝塞尔曲线
│ ├── IndexedPolycurve ← 索引多段曲线
│ │ └── Polyline ← 折线
│ │ └── Polygon ← 封闭多边形
│ ├── EllipticalArc ← 椭圆弧
│ └── TrimmedCurve<TBasis> (abstract) ← 裁剪曲线
│ ├── Line : TrimmedCurve<InfiniteLine> ← 线段
│ └── Arc : TrimmedCurve<Circle> ← 圆弧
核心设计模式:TrimmedCurve<TBasis> 泛型类表示”从基曲线(Basis Curve)上截取一段”的裁剪曲线。例如 Line 继承 TrimmedCurve<InfiniteLine>,表示 Line 是从一条无限直线(InfiniteLine)上截取的有限线段;Arc 继承 TrimmedCurve<Circle>,表示 Arc 是从一个完整圆上截取的圆弧段。
3.3.2 Curve:抽象基类
Curve 定义所有曲线的公共契约(源码 Curve.cs:12-46):
public abstract partial class Curve : ICurve, ITransformable<Curve>
{
// 在参数 u 处求点
public abstract Vector3 PointAt(double u);
// 在参数 u 处求变换(Z 轴沿曲线切线方向)
public abstract Transform TransformAt(double u);
// 返回变换后的曲线副本
public abstract Curve Transformed(Transform transform);
// 沿曲线从起点出发指定距离,返回对应的参数
public abstract double ParameterAtDistanceFromParameter(double distance, double start);
// 求与另一曲线的交点
public abstract bool Intersects(ICurve curve, out List<Vector3> results);
}
Intersects 方法是 Elements 2.1 版大幅扩展的功能,采用双分派(double dispatch)模式:每个具体曲线类型的 Intersects 方法内部通过 switch 判断另一曲线的类型,将调用路由到最适合的求交算法。
3.3.3 BoundedCurve:有界曲线
BoundedCurve 在 Curve 基础上增加了”有起点和终点”的语义(源码 BoundedCurve.cs):
public abstract class BoundedCurve : Curve, IBoundedCurve
{
public virtual Vector3 Start { get; protected set; } // 起点
public virtual Vector3 End { get; protected set; } // 终点
public virtual Domain1d Domain => new Domain1d(0, Length()); // 参数域
public const double DefaultMinimumChordLength = 0.1; // 默认弦长
public abstract BBox3 Bounds(); // 包围盒
public abstract double Length(); // 总长度
public abstract double ArcLength(double start, double end); // 指定参数间的弧长
public virtual Vector3 Mid() => PointAt(this.Domain.Mid()); // 中点
// 转换为折线近似(用于渲染和三角剖分)
public virtual Polyline ToPolyline(int divisions = 10) { ... }
// 归一化参数访问(u ∈ [0, 1])
public Vector3 PointAtNormalized(double u) { ... }
public Transform TransformAtNormalized(double u) { ... }
}
Domain 属性定义曲线的参数范围。对于 Line,参数域为 [0, Length](从起点沿线的距离);对于 Bezier,参数域为 [0, 1](归一化)。
3.3.4 Line:线段
Line 继承自 TrimmedCurve<InfiniteLine>,表示一条有起点和终点的直线段。其 BasisCurve 是一条 InfiniteLine,参数域为 [0, Length](Domain.Min 为起点到 BasisCurve 原点沿方向的距离)。
// 默认构造函数:沿 X 轴的单位长度线段
var defaultLine = new Line(); // Start=(0,0,0), End=(1,0,0)
// 标准构造
var l1 = new Line((0, 0, 0), (10, 0, 0));
// 从起点沿方向指定长度
var l2 = new Line((0, 5, 0), Vector3.XAxis, 20); // 从(0,5,0)沿 X 方向 20 单位
// 从 InfiniteLine 裁剪
var infLine = new InfiniteLine((0, 0, 0), Vector3.XAxis);
var l3 = new Line(infLine); // 自动从 BasisCurve.Domain 确定起止点
核心方法:
var line = new Line((0, 0, 0), (10, 0, 0));
// 方向(单位向量)
Vector3 dir = line.Direction(); // (1, 0, 0)
// 长度
double len = line.Length(); // 10
// 某参数处的点(参数 = 沿线段距离)
Vector3 p = line.PointAt(3.5); // (3.5, 0, 0)
// 线段中点
Vector3 mid = line.Mid(); // (5, 0, 0)
// 端点信息
Vector3 s = line.Start; // (0, 0, 0)
Vector3 e = line.End; // (10, 0, 0)
// 反转方向
Line rev = line.Reversed(); // Start=(10,0,0), End=(0,0,0)
// 偏移(保持方向,垂直于 Z 轴方向偏移)
Line offset = line.Offset(2.0, flip: false);
// 加厚为多边形(线段→矩形面)
Polygon thick = line.Thicken(1.0); // 1单位宽的矩形条
// 等分线段
var segments = line.DivideIntoEqualSegments(5); // 分为 5 段,返回 5 条 Line
var byLength = line.DivideByLength(3.0); // 按长度 3 划分
// 包围盒
BBox3 bbox = line.Bounds();
3.3.5 InfiniteLine:无限直线
InfiniteLine 直接继承自 Curve,没有起点和终点。参数化从 -∞(Origin 之前)到 +∞(Origin 之后),参数为 0 时对应 Origin 点。
var infLine = new InfiniteLine((5, 0, 0), Vector3.YAxis);
// 在参数 t 处求点
Vector3 p0 = infLine.PointAt(0); // (5, 0, 0) —— Origin
Vector3 p1 = infLine.PointAt(3); // (5, 3, 0)
Vector3 p2 = infLine.PointAt(-2); // (5, -2, 0)
// 判断某点是否在线上
bool onLine = infLine.ParameterAt((5, 10, 0), out double t); // true, t=10
3.3.6 Arc:圆弧
Arc 继承自 TrimmedCurve<Circle>,从完整圆上截取一个角度区间形成圆弧。圆弧沿逆时针方向从 StartAngle 扫到 EndAngle。
// 在 XY 平面,圆心在原点,半径 5,从 0° 到 90° 逆时针
var arc1 = new Arc(Vector3.Origin, 5.0, 0, 90);
// 圆心在 (10,0,0),半径 3,从 45° 到 180°
var arc2 = new Arc((10, 0, 0), 3.0, 45, 180);
// 从 Circle 裁剪
var circle = new Circle((0, 0, 0), 5.0);
var arc3 = new Arc(circle, 0, Math.PI); // 半圆弧,参数为弧度
// 通过三点创建
var arc4 = Arc.ByThreePoints((0,0,0), (5,5,0), (10,0,0)); // 过三点的弧
// 两直线间的倒角弧(Fillet)
var a = new Line((0,0,0), (10,0,0));
var b = new Line((10,0,0), (10,10,0));
var fillet = Arc.Fillet(a, b, 2.0); // 半径 2 的倒角弧
// 核心属性
double radius = arc1.Radius; // 5
Vector3 center = arc1.Center; // (0,0,0)
Vector3 start = arc1.Start; // (5,0,0)(0°处)
Vector3 end = arc1.End; // (0,5,0)(90°处)
Vector3 mid = arc1.Mid(); // 中点(45°处)
double sAngle = arc1.StartAngle; // 0
double eAngle = arc1.EndAngle; // 90
// 弧长
double arcLen = arc1.Length(); // (π/2) * 5 ≈ 7.854
// 互补弧(360° 减去当前弧)
Arc complement = arc1.Complement(); // 从 90° 到 360°
// 反转方向
Arc reversed = arc1.Reversed(); // 从 90° 到 0°
内部机制:如果用户提供的 endAngle < startAngle(比如 270°→90° 表示扫过 180°),内部会调用 EnsureIncreasingAngleDomain 将参数翻转并对底层圆做 180° 旋转,保证参数域始终递增。
3.3.7 Circle:圆
Circle 直接继承 Curve,参数化范围 0 → 2π。它通过一个 Transform 描述其在空间中的位置和朝向(圆心 = Transform.Origin,法向量 = Transform.ZAxis)。
// XY 平面原点处、默认半径 1
var c1 = new Circle(); // Center=(0,0,0), Radius=1
// 指定圆心和半径
var c2 = new Circle((10, 0, 5), 3.0); // Center=(10,0,5), Radius=3
// 指定 Transform(可用于任意朝向的圆)
var transform = new Transform((5,5,0), Vector3.YAxis); // 在 Y 轴方向上的圆
var c3 = new Circle(transform, 2.0);
// 求点(参数 u 为弧度,从 +X 方向逆时针)
Vector3 p0 = c2.PointAt(0); // (13, 0, 5)
Vector3 p90 = c2.PointAt(Math.PI / 2); // (10, 3, 5)
// 在参数 u 处的变换(用于沿曲线放置截面)
Transform t = c2.TransformAt(Math.PI / 4);
// 隐式转换为 Arc(一个完整的 360° 弧)
Arc fullArc = c2; // implicit operator
3.3.8 Ellipse:椭圆
Ellipse 直接继承 Curve,结构与 Circle 类似但区分长轴(MajorAxis)和短轴(MinorAxis)。
// 默认:长轴=2(X方向),短轴=1(Y方向)
var e1 = new Ellipse();
// 指定中心和轴长
var e2 = new Ellipse((0, 0, 0), majorAxis: 5.0, minorAxis: 3.0);
// 指定 Transform 和轴长
var e3 = new Ellipse(new Transform((5,0,0)), 4.0, 2.0);
// 求点
Vector3 p0 = e2.PointAt(0); // (5, 0, 0)
Vector3 pHalfPi = e2.PointAt(Math.PI / 2); // (0, 3, 0)
// 近似周长(Ramanujan 公式)
double circ = e2.Circumference();
// 判断点是否在椭圆上
bool onEllipse = e2.ParameterAt((3, 2.4, 0), out double t);
椭圆的弧长计算使用数值积分(辛普森法),在 ArcLength 方法中实现。
3.3.9 Bezier:贝塞尔曲线
Bezier 继承 BoundedCurve,支持任意阶的贝塞尔曲线(至少 3 个控制点)。参数化范围 0 → 1。
// 三次贝塞尔曲线(4 个控制点通常称为 cubic Bezier)
var bezier = new Bezier(new List<Vector3>
{
(0, 0, 0), // P0: 起点
(3, 5, 0), // P1: 控制点
(7, 5, 0), // P2: 控制点
(10, 0, 0) // P3: 终点
});
// 更高阶贝塞尔
var bezier5 = new Bezier(new List<Vector3>
{
(0,0,0), (2,3,0), (5,4,0), (7,3,0), (10,0,0)
});
// 求点(u ∈ [0, 1])
Vector3 mid = bezier.PointAt(0.5);
// 曲线长度(分段线性近似)
double len = bezier.Length();
// Frenet 标架:切线、法线、副法线
Vector3 tangent = bezier.TangentAt(0.3);
Vector3 normal = bezier.NormalAt(0.3);
Vector3 binormal = bezier.BinormalAt(0.3);
// 速度与加速度向量
Vector3 vel = bezier.VelocityAt(0.5);
Vector3 accel = bezier.AccelerationAt(0.5);
// Frametype: Frenet 或 RoadLike
var roadBezier = new Bezier(controlPoints, FrameType.RoadLike);
Bezier 实现了完整的伯恩斯坦多项式计算:PointAt 方法通过伯恩斯坦基函数和组合数(二项式系数)来计算曲线上任意参数位置的点。
3.3.10 Polyline:折线
Polyline 继承 IndexedPolycurve → BoundedCurve,表示由连续线段组成的开放折线。参数化范围 0 → n−1(n 为顶点数量),每个整数段对应一条线段。
// 从顶点列表创建
var polyline = new Polyline(new List<Vector3>
{
(0, 0, 0),
(10, 0, 0),
(10, 10, 0),
(0, 10, 0)
});
// 便捷构造函数(params 形式)
var pl = new Polyline((0,0,0), (10,0,0), (10,10,0));
// 获取线段
Line[] segs = polyline.Segments(); // 3 条线段
// 长度(所有线段之和)
double len = polyline.Length();
// 某参数处的点
Vector3 p = polyline.PointAt(1.5); // 在第1段中点
// 反转
Polyline rev = polyline.Reversed();
// 偏移(产生一个闭合 Polygon)
Polygon[] offsets = polyline.Offset(2.0, EndType.Butt);
// 单侧偏移
Polygon[] sideOffsets = polyline.OffsetOnSide(1.0, flip: false);
// 平面
Plane plane = polyline.Plane();
// 获取子段
Polyline sub = polyline.GetSubsegment((5,0,0), (10,5,0));
// 判断线段相交
bool intersects = polyline.Intersects(someLine, out List<Vector3> intersections);
// 在指定点处拆分
polyline.Split(new List<Vector3> { (5, 0, 0), (10, 5, 0) });
3.4 曲线的切分与求交
Elements 2.1 版对求交系统进行了大幅扩展。每个曲线类型都实现了大量的 Intersects 重载,覆盖了各种曲线组合。
3.4.1 求交系统的设计
所有求交操作的统一入口是基类的抽象方法:
public abstract bool Intersects(ICurve curve, out List<Vector3> results);
具体实现采用访问者模式(Visitor Pattern)的简化版——在每个具体曲线类型的 Intersects(ICurve, ...) 方法中,通过 switch 针对不同的曲线类型调用不同的专用算法。
以 Circle.Intersects 为例(源码 Circle.cs:191-206):
public override bool Intersects(ICurve curve, out List<Vector3> results)
{
switch (curve)
{
case BoundedCurve boundedCurve:
return boundedCurve.Intersects(this, out results); // 委托给有界曲线
case InfiniteLine line:
return Intersects(line, out results); // 圆与无限直线
case Circle circle:
return Intersects(circle, out results); // 圆与圆
case Ellipse ellipse:
return Intersects(ellipse, out results); // 圆与椭圆
default:
throw new NotImplementedException();
}
}
TrimmedCurve<TBasis>.Intersects 的方法更加复杂(源码 TrimmedCurve.cs:28-51),覆盖了 Line、Arc、EllipticalArc、InfiniteLine、Circle、Ellipse、IndexedPolycurve、Bezier 等所有常见组合。
3.4.2 线段与篮框求交
Line.Intersects(BBox3 box, ...) 使用经典的 slab test 算法,分别计算线段与包围盒在 X、Y、Z 三个轴向上的进入/离开参数 t,然后求交集:
var line = new Line((0, 0, 0), (10, 10, 0));
var bbox = new BBox3((2, 2, -1), (8, 8, 1));
bool hits = line.Intersects(bbox, out List<Vector3> results);
// results: [ (2,2,0), (8,8,0) ] —— 线段穿过包围盒的两个交点
3.4.3 线段与线段求交(3D)
Line.Intersects(Line, ...) 支持有限线段和无限直线两种模式:
var line1 = new Line((0, 0, 0), (10, 0, 0));
var line2 = new Line((5, -5, 0), (5, 5, 0));
// 有限线段求交
bool hit = line1.Intersects(line2, out Vector3 pt); // true, pt=(5,0,0)
// 无限直线求交
bool hitInf = line1.Intersects(line2, out Vector3 pt2, infinite: true);
// 线段与 BBox 求交(无限模式)
bool hitBox = line1.Intersects(bbox, out List<Vector3> results, infinite: true);
3D 线段求交的算法(源码 Line.cs:318-370)使用了一个巧妙的几何方法:将两条线段视为两条 3D 直线,通过构造包含第一条线与第二条线端点(或反过来)的平面来判断它们是否共面且相交。不是简单的参数方程求解,而是通过平面交线的方法,避免了平行或异面情况下的数值问题。
3.4.4 圆与圆的求交
两个圆的求交(Circle.Intersects(Circle, ...))分两种情况:
- 共面圆:通过构造三角形(已知两圆半径和圆心距),用余弦定理求交点
- 非共面圆:先求两圆所在平面的交线,再分别求该交线与两圆的交点,取同时在两圆上的点
var c1 = new Circle((0, 0, 0), 5.0);
var c2 = new Circle((6, 0, 0), 5.0); // 两圆相交
bool hit = c1.Intersects(c2, out List<Vector3> results);
// results: 两个交点
3.4.5 曲线与 Bezier 的求交
对于 Bezier 曲线,Elements 采用了迭代逼近 + 细分算法来处理各种求交场景:
Bezier.Intersects(InfiniteLine, ...):迭代寻找 Bezier 上到直线距离为 0 的点Bezier.Intersects(Circle, ...):迭代寻找 Bezier 上到圆心的距离等于半径的点Bezier.Intersects(Ellipse, ...):迭代寻找 Bezier 上满足椭圆方程的点Bezier.Intersects(Bezier, ...):使用递归包围盒细分算法——将两条 Bezier 各自递归细分为更小的曲线段,丢弃包围盒不重叠的段,直到包围盒小于容差时取中心点为交点
var b1 = new Bezier(new List<Vector3> { (0,0,0), (2,5,0), (8,5,0), (10,0,0) });
var b2 = new Bezier(new List<Vector3> { (0,5,0), (3,-2,0), (7,-2,0), (10,5,0) });
bool hit = b1.Intersects(b2, out List<Vector3> results);
// results: 两条三次贝塞尔在 XY 平面交点的迭代近似解
3.4.6 椭圆与直线/圆的求交
Ellipse.Intersects(InfiniteLine, ...) 的处理很优雅:将直线变换到椭圆的局部坐标系,然后解一个一元二次方程来求交点。
Ellipse.Intersects(Circle, ...) 在共面时使用迭代逼近;在多平面时分别处理。对于椭圆与椭圆的求交,采用了同样的策略——迭代逼近搜索满足两个椭圆方程的参数。
3.5 Polygon:布尔运算集合的多边形
Polygon 继承自 Polyline,要求顶点共面且封闭(最后一个顶点隐含连接回第一个顶点)。它是 Elements 中进行面域运算的核心类型。
3.5.1 构造与验证
// 标准构造
var rect = new Polygon(new List<Vector3>
{
(0, 0, 0),
(10, 0, 0),
(10, 5, 0),
(0, 5, 0)
});
// 便捷构造(params 形式)
var triangle = new Polygon((0,0,0), (3,0,0), (1.5, 2.5, 0));
// 构造时自动验证:
// 1. 顶点共面检查——非共面顶点会抛出 ArgumentException
// 2. 移除连续重复顶点
// 3. 删除重叠边上的冗余顶点
// 4. 自相交检查
// 5. 至少需要 3 个顶点
ValidateVertices() 方法(源码 Polygon.cs:72-89)在构造时自动调用,确保多边形的几何正确性。
3.5.2 布尔运算(Union、Intersection、Difference、XOR)
Elements 的多边形布尔运算底层使用 Clipper 库(ClipperLib)。Clipper 是一个经过广泛验证的 2D 多边形布尔运算库(基于 Vatti 算法),Elements 通过整数缩放的方式将浮点多边形转换为 Clipper 能处理的整数坐标:
// ToClipperPath 的核心转换(源码 PolygonExtensions.cs:20-28)
internal static List<IntPoint> ToClipperPath(this Polygon p, double tolerance = Vector3.EPSILON)
{
var scale = Math.Round(1.0 / tolerance);
var path = new List<IntPoint>();
foreach (var v in p.Vertices)
{
path.Add(new IntPoint(
Math.Round(v.X * scale),
Math.Round(v.Y * scale)));
}
return path;
}
缩放因子为 1 / EPSILON ≈ 100000,这意味着所有坐标被放大 10 万倍后转为整数参与 Clipper 运算,运算完成后再缩回来。
四个布尔运算:
var a = Polygon.Rectangle((0, 0), (10, 10));
var b = Polygon.Rectangle((5, 5), (15, 15));
// 交集 (Intersection)
var intersection = a.Intersection(b);
// 返回两个多边形重叠的部分:一个 5×5 的正方形
// 并集 (Union)
var union = a.Union(b);
// 返回两个多边形合并为一个 L 形
// 差集 (Difference) — a 减去 b
var diff = a.Difference(b);
// 从矩形 a 中挖掉与 b 重叠的部分
// 对称差 (XOR)
var xor = Polygon.XOR(new[] { a }, new[] { b });
// 返回两个多边形不重叠的部分
// 静态多集合版本:
var allUnion = Polygon.UnionAll(polygonList);
var setDiff = Polygon.Difference(setA, setB);
var setIntersection = Polygon.Intersection(setA, setB);
var setXor = Polygon.XOR(setA, setB);
布尔运算支持 VoidTreatment 参数。IgnoreInternalVoids 将所有多边形视为实心(NonZero 填充规则),PreserveInternalVoids 保留内部的孔洞(EvenOdd 填充规则)。
3.5.3 Contains:点在多边形内的判断
var rect = Polygon.Rectangle((0, 0), (10, 10));
// 简单判断
bool inside = rect.Contains((5, 5, 0)); // true
// 带包含状态详情
rect.Contains((10, 5, 0), out Containment c);
// c == Containment.CoincidesAtEdge —— 点恰好在边缘上
rect.Contains((0, 0, 0), out Containment c2);
// c2 == Containment.CoincidesAtVertex —— 点在顶点上
rect.Contains((-1, 5, 0), out Containment c3);
// c3 == Containment.Outside —— 在多边形外部
内部实现使用绕组数(winding number)算法(源码 Polygon.cs:586-629),支持 3D 空间中的任意朝向多边形——通过投影变换到局部 XY 平面后进行 2D 判断。
3.5.4 面积与法向量
var rect = Polygon.Rectangle((0, 0), (10, 5));
double area = rect.Area(); // 50(绝对值)
double signedArea = rect.Area(signed: true); // 正=逆时针, 负=顺时针
Vector3 normal = rect.Normal(); // (0, 0, 1) —— 根据 Newell 法计算的单位法向量
bool cw = rect.IsClockWise(); // 判断多边形环绕方向
Area() 使用鞋带公式(Shoelace formula),对于 3D 朝向的多边形,先变换到局部 XY 平面再计算。
3.5.5 Offset:多边形偏移
var rect = Polygon.Rectangle((0, 0), (10, 10));
// 向外偏移 2 单位(产生一个带圆角的更大矩形)
Polygon[] expanded = rect.Offset(2.0);
// 向内偏移(收缩)
Polygon[] shrunk = rect.Offset(-1.5);
Offset 底层同样使用 Clipper 库的 ClipperOffset,支持不同的端点类型(EndType):Butt(平头)、Square(方形延伸)、ClosedPolygon(封闭多边形)。
3.5.6 其他多边形操作
var rect = Polygon.Rectangle((0, 0), (10, 10));
// 形状中心(质心,考虑面积的加权中心)
Vector3 centroid = rect.Centroid(); // (5, 5, 0)
// 顶点平均值中心
Vector3 center = rect.Center(); // (5, 5, 0)
// 变换
var moved = rect.TransformedPolygon(new Transform((0, 0, 10)));
// 反转环绕方向
var rev = rect.Reversed();
// 投影到平面
var proj = rect.Project(new Plane(Vector3.Origin, Vector3.YAxis));
// 去除共线顶点
var cleaned = rect.CollinearPointsRemoved();
// 倒角(所有角)
var filleted = rect.Fillet(1.0); // 所有角用半径 1 的圆弧替换
// 多边形是否覆盖/包含/触碰其他多边形
bool covers = outer.Covers(inner);
bool disjoint = outer.Disjoint(inner);
bool touches = outer.Touches(inner);
// 确定一个内部点(用于标签放置等)
Vector3 internalPt = rect.PointInternal();
// 与平面求交
bool hits = rect.Intersects(somePlane, out List<Vector3> intersections);
// 用平面裁剪多边形
List<Polygon> trimmed = rect.Trimmed(cutPlane);
3.5.7 静态工厂方法
Elements 提供了多个静态工厂方法来创建常用多边形:
// 矩形(两点定义)
var rect = Polygon.Rectangle((0, 0), (10, 5));
// 矩形(含 Z 坐标)
var rect3D = Polygon.Rectangle((0, 0, 3), (10, 5, 3));
// N 边形(正多边形)
var hexagon = Polygon.NGon(6, radius: 5.0); // 正六边形,外接圆半径 5
// L 形
var lShape = Polygon.L(10, 8, 2); // 宽10高8厚2的 L 形
// 圆形 → 多边形(见 3.6 节)
3.6 从圆形曲线转换为 Polygon
当需要将圆、椭圆、圆弧等曲线类型用于三角剖分(Tessellation)或布尔运算(它们只能处理多边形)时,就需要 ToPolygon 方法。
3.6.1 Circle.ToPolygon
var circle = new Circle((0, 0, 0), 5.0);
// 默认 10 段(正十边形近似)
var decagon = circle.ToPolygon();
// 指定分段数
var poly36 = circle.ToPolygon(divisions: 36); // 36段近似——更平滑
// 源码实现(Circle.cs:81-95)
public Polygon ToPolygon(int divisions = 10)
{
var pts = new List<Vector3>();
var twoPi = Math.PI * 2;
var step = twoPi / divisions;
// 使用 EPSILON 确保最后一帧不会恰好走到 2π
for (var t = 0.0; t < twoPi - Vector3.EPSILON; t += step)
{
pts.Add(this.PointAt(t));
}
return new Polygon(pts, true); // disableValidation=true,避免自相交误报
}
3.6.2 Ellipse 的 ToPolygon
Ellipse 没有直接的 ToPolygon 方法,但可以借助 BoundedCurve.ToPolyline 然后闭合:
var ellipse = new Ellipse((0, 0, 0), 5.0, 3.0);
// ToPolyline 返回开放折线,需要手动闭合
var polyline = ellipse.ToPolyline(divisions: 24);
var polygon = new Polygon(polyline.Vertices);
// 或者通过 EllipticalArc 完整闭合
BoundedCurve.ToPolyline 的源码逻辑(BoundedCurve.cs:92-108)是在参数域内均匀采样,然后用这些采样点构造折线。对于圆和椭圆,采样点不均匀分布在弧长上,而是均匀分布在角度参数上——这意味着在曲率更大的区域(椭圆短轴端点附近)采样会更密,这是合理的近似策略。
3.6.3 Arc 的 ToPolygon
Arc 也继承自 BoundedCurve,所以可以使用 ToPolyline:
var arc = new Arc(Vector3.Origin, 5.0, 0, 180); // 半圆弧
var pl = arc.ToPolyline(divisions: 20);
// 如果要封闭为扇形,可以手动添加圆心
var fanVerts = new List<Vector3> { arc.Center };
fanVerts.AddRange(pl.Vertices);
var sector = new Polygon(fanVerts);
3.6.4 应用场景
ToPolygon(或 ToPolyline)的结果主要用于:
- 三角剖分:Elements 的
Tessellation模块只能处理多边形面,需要通过ToPolygon将曲线转化为多边形后再送入三角剖分器 - 布尔运算:Clipper 只能处理 2D 多边形,圆的布尔运算需要先多边形化
- 渲染:GPU 只能处理直线段,曲线显示依赖多边形近似
选择合适的分段数是关键:分段太少会导致可见的棱角,太多则会增加不必要的三角形数量。对于建筑应用,建议的分段数经验值:
| 曲线类型 | 建议分段数 | 说明 |
|---|---|---|
| 小半径弧 (R < 0.5m) | 8~12 | 视觉上已经足够平滑 |
| 中半径弧 (0.5m < R < 5m) | 16~24 | 常规建筑弧形元素 |
| 大半径弧 (R > 5m) | 32~48 | 大型弧形幕墙、屋顶 |
| 完整圆 | 24~48 | 按建筑尺度调整 |
| 椭圆 | 32~64 | 长轴方向的曲率变化大,需要更多分段 |