第11章:空间数据结构——网格、拓扑与自适应网格
Elements 的空间命名空间(Elements.Spatial)提供了一套完整的数据结构,用于描述建筑中的空间关系、网格划分、三维拓扑和管线寻路。这些工具是参数化建筑设计中不可或缺的基础设施——从柱网布局到幕墙分格,从剖面交线到机电管线自动排布,都依赖它们。
本章将依次介绍六个核心组件:
| 组件 | 维度 | 用途 |
|---|---|---|
Grid1d |
1D | 轴线划分、幕墙竖梃定位、楼层标高定义 |
Grid2d |
2D | 柱网布局、幕墙面板排列、楼板分区 |
HalfEdgeGraph2d |
2D | 剖面交线→多边形重建 |
CellComplex |
3D | 建筑空间的三维拓扑表示 |
AdaptiveGrid |
3D | 动态细分的三维网格,MEP 管线寻路 |
AdaptiveGraphRouting |
3D | 基于加权图的最优路径搜索算法 |
11.1 Grid1d:一维网格
11.1.1 核心概念
Grid1d 模拟一条”数轴”,可以在其上划分区间。每个区间(Cell)可以继续递归细分,形成层次化的网格结构。其核心属性是 Domain(Domain1d 类型)——描述该网格覆盖的数值范围 [Min, Max]。
namespace Elements.Spatial
{
public class Grid1d
{
public Domain1d Domain { get; } // 数值域 [Min, Max]
public string Type { get; set; } // 可选的类型标记
public List<Grid1d> Cells { get; } // 子网格(null 表示无细分)
public bool IsSingleCell { get; } // 是否无子划分
public BoundedCurve Curve { get; } // 关联的几何曲线(常为 Line)
}
}
Domain1d 是一个简单的区间结构:
public struct Domain1d
{
public double Min { get; }
public double Max { get; }
public double Length => Max - Min;
// 在 position 处分割为两个子域
public Domain1d[] SplitAt(double position);
// 均分为 n 份
public Domain1d[] DivideByCount(int n);
// 判断值是否在区间内
public bool Includes(double value);
}
11.1.2 构造 Grid1d
四种构造方式对应不同使用场景:
// 1. 纯数值域(默认沿 X 轴,长度 1.0)
var gridNumber = new Grid1d(10.0); // [0, 10]
// 2. 从 Domain1d 构造
var domain = new Domain1d(3.0, 15.0); // [3, 15]
var gridDomain = new Grid1d(domain);
// 3. 从几何曲线构造(使用曲线自身域)
var line = new Line(new Vector3(0, 0, 0), new Vector3(5, 5, 0));
var gridCurve = new Grid1d(line);
// 4. 从已有 Grid1d 拷贝
var gridCopy = new Grid1d(gridNumber);
11.1.3 分割操作
Grid1d 提供丰富的分割方法,以适应不同的设计需求:
按绝对位置分割:
var grid = new Grid1d(10.0);
// 在位置 3.0 处分割 → 生成 [0, 3] 和 [3, 10] 两个子格
grid.SplitAtPosition(3.0);
// 批量分割
grid.SplitAtPositions(new[] { 2.0, 5.0, 8.0 });
按归一化参数分割:
// 在域中点处分割(t=0.5)
grid.SplitAtParameter(0.5);
// 在 1/3 和 2/3 处分割
grid.SplitAtParameters(new[] { 1.0 / 3.0, 2.0 / 3.0 });
按偏移量分割(从起点或终点测量):
// 从起点 2.5 米处分割
grid.SplitAtOffset(2.5);
// 从终点 1.0 米处分割
grid.SplitAtOffset(1.0, fromEnd: true);
按世界空间点分割:
// 找到曲线上最接近该点的位置并分割
var pt = new Vector3(3, 2, 0);
grid.SplitAtPoint(pt);
11.1.4 等分与模式化划分
这是建筑设计中最高频的使用场景——幕墙分格、柱距排布、栏杆竖梃等:
按数量等分:
var grid = new Grid1d(12.0);
grid.DivideByCount(6); // 均分为 6 段,每段 2.0m
// 访问子格
foreach (var cell in grid.Cells)
{
Console.WriteLine($"[{cell.Domain.Min:F2}, {cell.Domain.Max:F2}]");
}
// 输出: [0.00, 2.00] [2.00, 4.00] ... [10.00, 12.00]
按近似长度等分(三种取整模式):
var grid = new Grid1d(10.0);
// 最接近模式:10/3≈3.33 → 3 段,每段 3.33m
grid.DivideByApproximateLength(3.0, EvenDivisionMode.Nearest);
// 向上取整:保证每段 ≤ 目标长度(更密)
grid.DivideByApproximateLength(3.0, EvenDivisionMode.RoundUp); // 4 段
// 向下取整:保证每段 ≥ 目标长度(更稀)
grid.DivideByApproximateLength(3.0, EvenDivisionMode.RoundDown); // 3 段
按固定长度划分(四种余数处理方式):
FixedDivisionMode 枚举定义了余数段的放置策略:
var grid = new Grid1d(10.0);
// 余数放在末尾(默认):3 段 × 3m + 1m 余数在末尾
grid.DivideByFixedLength(3.0, FixedDivisionMode.RemainderAtEnd);
// 余数放在起始
grid.DivideByFixedLength(3.0, FixedDivisionMode.RemainderAtStart);
// 余数均分到两端:0.5m + 3 段 × 3m + 0.5m
grid.DivideByFixedLength(3.0, FixedDivisionMode.RemainderAtBothEnds);
// 余数靠近中间
grid.DivideByFixedLength(3.0, FixedDivisionMode.RemainderNearMiddle);
sacrificialPanels 参数可以牺牲若干完整面板,使余数段更长(减少过短面板):
// 牺牲 1 个完整面板:2 段 × 3m + 4m 余数
grid.DivideByFixedLength(3.0, sacrificialPanels: 1);
按模式划分(三种重复模式):
这是幕墙设计中变化节奏的实现方式:
var grid = new Grid1d(15.0);
// 循环模式 [A, B, C, A, B, C, ...]
grid.DivideByPattern(
new[] { 2.0, 1.5, 1.0 },
PatternMode.Cycle,
FixedDivisionMode.RemainderAtEnd
);
// 翻转模式 [A, B, C, B, A, B, C, B, A, ...]
grid.DivideByPattern(
new[] { 2.0, 1.5, 1.0 },
PatternMode.Flip,
FixedDivisionMode.RemainderAtEnd
);
// 不重复模式 [A, B, C],仅应用一次,余数按策略处理
grid.DivideByPattern(
new[] { 2.0, 1.5, 1.0 },
PatternMode.None,
FixedDivisionMode.RemainderAtEnd
);
带类型名的模式划分——直接用数字索引标记每个面板的类型:
var pattern = new List<(string typeName, double length)>
{
("窄板", 1.2),
("宽板", 2.4),
("标准板", 1.8)
};
grid.DivideByPattern(pattern, PatternMode.Cycle);
// 遍历子格时,每个 cell.Type 会包含对应的类型名
foreach (var cell in grid.GetCells())
{
Console.WriteLine($"类型: {cell.Type}, 长度: {cell.Domain.Length}");
}
11.1.5 子格遍历
var grid = new Grid1d(10.0);
grid.DivideByCount(4);
// 仅顶层子格
foreach (var cell in grid.Cells) { /* 4 个 */ }
// 递归获取所有叶子格
foreach (var cell in grid.GetCells()) { /* 也是 4 个(因为未二次细分) */ }
// 获取所有分隔点位置
var domains = grid.GetCellDomains(recursive: true);
// [0, 2.5, 5.0, 7.5, 10.0]
// 获取分隔点世界坐标
var separators = grid.GetCellSeparators(recursive: true);
11.1.6 Grid1d 的类型系统(Fixed vs Custom)
Grid1d 有两种语义类型:
- Fixed 类型:通过
DivideByCount、DivideByApproximateLength、DivideByFixedLength等方法创建的子格。这些方法产生的子格Type默认为null - Custom 类型:通过
DivideByPattern创建的子格,每个子格的Type被设置为模式中对应的typeName
Type 属性通过 Grid2d.SpawnSubGrid 传递到二维网格单元中(U/V 两侧的 Type 用 ` / ` 连接)。
11.1.7 实战:轴线排列与幕墙竖梃定位
using Elements;
using Elements.Geometry;
using Elements.Spatial;
// --- 场景 1:建筑轴线定义 ---
// 沿 30 米建筑长轴方向定义轴线
var axisX = new Grid1d(new Line(
new Vector3(0, 0, 0),
new Vector3(30, 0, 0)
));
// 按 8 米间距划分柱跨,余数放在两端
axisX.DivideByFixedLength(8.0, FixedDivisionMode.RemainderAtBothEnds);
Console.WriteLine("X 向轴线位置:");
foreach (var cell in axisX.GetCells())
{
Console.WriteLine($" 轴线在 X={cell.Domain.Min:F1}m - X={cell.Domain.Max:F1}m");
}
// 输出: [0, 3], [3, 11], [11, 19], [19, 27], [27, 30]
// --- 场景 2:幕墙竖梃定位 ---
var facadeGrid = new Grid1d(24.0); // 24 米长幕墙
// 使用翻转模式创造韵律:[1.2, 2.4, 1.2, 2.4, ...]
facadeGrid.DivideByPattern(
new[] { 1.2, 2.4 },
PatternMode.Flip
);
// 获取竖梃位置点
var mullionPoints = facadeGrid.GetCellSeparators(recursive: true);
Console.WriteLine($"\n竖梃数量: {mullionPoints.Count} 个");
foreach (var pt in mullionPoints)
{
Console.WriteLine($" 竖梃 @ ({pt.X:F1}, {pt.Y:F1}, {pt.Z:F1})");
}
11.2 Grid2d:二维网格
11.2.1 核心概念
Grid2d 由 U 和 V 两个方向上的 Grid1d 组合而成,形成矩形网格单元。每个单元是一个独立的 Grid2d 对象,同样支持递归细分。
namespace Elements.Spatial
{
public class Grid2d
{
public Grid1d U { get; } // U 方向一维网格
public Grid1d V { get; } // V 方向一维网格
public string Type { get; set; } // 单元类型标记
public bool IsSingleCell { get; } // 是否无子划分
// 通过 [u, v] 索引访问顶层单元
public Grid2d this[int u, int v] { get; }
// 获取所有顶层单元(二维列表)
public List<List<Grid2d>> Cells { get; }
}
}
11.2.2 构造 Grid2d
按尺寸构造:
// 最简单的构造:U 方向 20m,V 方向 15m
var grid = new Grid2d(20, 15);
// 用独立的 Grid1d 作为 U/V 轴
var uAxis = new Grid1d(new Line(
new Vector3(0, 0, 0),
new Vector3(30, 0, 0)
));
var vAxis = new Grid1d(new Line(
new Vector3(0, 0, 0),
new Vector3(0, 20, 0)
));
var grid = new Grid2d(uAxis, vAxis);
从多边形边界构造:
// 从单一边界构造——网格范围由边界的包围盒决定
var boundary = Polygon.Rectangle(
new Vector3(5, 5),
new Vector3(25, 15)
);
var grid = new Grid2d(boundary);
// 从多边界构造(例如含天井的建筑轮廓)
var outerBoundary = Polygon.Rectangle(new Vector3(0, 0), new Vector3(40, 30));
var courtyard = Polygon.Rectangle(new Vector3(15, 10), new Vector3(25, 20));
var grid = new Grid2d(new[] { outerBoundary, courtyard });
带自定义变换的构造:
// 在斜交平面上创建网格
var slopedBoundary = Polygon.Rectangle(new Vector3(0, 0, 0), new Vector3(10, 0, 10));
var transform = new Transform(
new Vector3(3, 5, 0), // 原点
new Vector3(1, 0, 0), // X 方向
new Vector3(0, 0, 1), // Z 方向作为 V
new Vector3(0, -1, 0) // Y 方向作为法线
);
var grid = new Grid2d(slopedBoundary, transform);
11.2.3 网格划分
对 Grid2d 的划分本质上是分别对其 U 和 V 方向的 Grid1d 进行划分:
var grid = new Grid2d(30, 20);
// 分别对 U 和 V 轴划分
grid.U.DivideByCount(6); // U 方向 6 跨
grid.V.DivideByCount(4); // V 方向 4 跨
Console.WriteLine($"网格单元总数: {grid.CellsFlat.Count}"); // 6 × 4 = 24
也可以直接在二维网格上分割:
// 在世界空间点处分割
grid.SplitAtPoint(new Vector3(10, 8, 0));
// 在网格坐标系的 (u=15, v=10) 处分割
grid.SplitAtPosition(15, 10);
11.2.4 单元访问
var grid = new Grid2d(30, 20);
grid.U.DivideByCount(6);
grid.V.DivideByCount(4);
// 索引访问
var cell = grid[2, 1]; // 第 2 列、第 1 行的单元
// 按位置查找
var foundCell = grid.FindCellAtPosition(12.5, 7.5);
// 获取整列
var column = grid.GetColumnAtIndex(3);
// 获取整行
var row = grid.GetRowAtIndex(2);
// 遍历所有叶子单元
foreach (var leafCell in grid.GetCells())
{
Console.WriteLine($"单元 [{leafCell.U.Domain.Min:F1}, {leafCell.U.Domain.Max:F1}] "
+ $"× [{leafCell.V.Domain.Min:F1}, {leafCell.V.Domain.Max:F1}]");
}
11.2.5 边界裁剪
当 Grid2d 从多边形构造时,单元可以被边界裁剪:
var LShape = Polygon.LShape(20, 15, 8, 10);
var grid = new Grid2d(LShape);
grid.U.DivideByCount(5);
grid.V.DivideByCount(4);
foreach (var cell in grid.GetCells())
{
if (cell.IsTrimmed())
{
// 获取裁剪后的几何——保留下在边界内的部分
var trimmed = cell.GetTrimmedCellGeometry();
// trimmed 可能是多个多边形(被洞口分割)
}
else if (cell.IsOutside())
{
// 完全在边界外的单元
}
else
{
// 完全在边界内的单元
var rect = cell.GetCellGeometry(); // 完整矩形
}
}
11.2.6 网格分隔线
// 获取 U 方向的分隔线(与 V 轴平行的线)
var uSeparators = grid.GetCellSeparators(GridDirection.U);
// 获取 V 方向的分隔线(与 U 轴平行的线),并裁剪到边界
var vSeparatorsTrimmed = grid.GetCellSeparators(GridDirection.V, trim: true);
11.2.7 实战:用 Grid2d 创建柱网布局
using Elements;
using Elements.Geometry;
using Elements.Geometry.Solids;
using Elements.Spatial;
var model = new Model();
// 1. 定义建筑网格(40m × 30m)
var columnGrid = new Grid2d(40, 30);
// 2. 分别划分 U 和 V 方向
columnGrid.U.DivideByFixedLength(8.0, FixedDivisionMode.RemainderAtBothEnds);
columnGrid.V.DivideByFixedLength(7.5, FixedDivisionMode.RemainderAtBothEnds);
// 3. 在每个网格单元中心放置柱子
var columnProfile = new Profile(Polygon.Rectangle(0.5, 0.5));
foreach (var cell in columnGrid.GetCells())
{
// 跳过被裁剪或超出边界的单元
if (cell.IsTrimmed() || cell.IsOutside()) continue;
// 获取单元矩形几何
var cellGeom = ((Polygon)cell.GetCellGeometry());
// 计算柱心位置(矩形中心点)
var center = cellGeom.Centroid();
center = new Vector3(center.X, center.Y, 0); // Z = 0 为柱底
// 创建柱子
var column = new Column(center, 4.5, columnProfile)
{
Name = $"柱-({cell.U.Domain.Min:F1},{cell.V.Domain.Min:F1})"
};
model.AddElement(column);
}
Console.WriteLine($"创建了 {model.AllElementsOfType<Column>().Count} 根柱子");
model.ToGlTF("column_grid.glb");
11.3 HalfEdgeGraph2d:半边图
11.3.1 核心概念
HalfEdgeGraph2d 是一个二维半边图结构,用于从相交的线段和多边形中重建封闭多边形。
半边图的核心组成:
- Vertex(顶点):二维空间中的唯一坐标点
- HalfEdge(半边):有向边,存储为
(from, to, tag?)三元组 - EdgesPerVertex:按起始顶点分组的所有半边列表
与标准半边结构的区别在于:Elements 的实现更轻量——不存储显式的 Face 引用,而是通过 Polygonize() 方法从图结构中提取封闭环路。
11.3.2 从多边形和剖切线构造
这是最典型的应用场景:用一个或多个横切线切割一个平面多边形,然后从交线重建分割后的子多边形。
using Elements.Geometry;
using Elements.Spatial;
// 定义一个 L 形剖面
var polygon = Polygon.LShape(10, 8, 4, 3);
// 定义一条穿过多边形的剖切线
var cutter = new Polyline(new[]
{
new Vector3(2, -1),
new Vector3(2, 9)
});
// 构造半边图
var graph = HalfEdgeGraph2d.Construct(polygon, cutter);
// 提取所有封闭多边形
var subPolygons = graph.Polygonize();
Console.WriteLine($"分割为 {subPolygons.Count} 个子多边形");
foreach (var poly in subPolygons)
{
Console.WriteLine($" 多边形面积: {poly.Area():F2} m²");
}
多剖切线示例:
// 三条剖切线横穿 L 形区域
var cutters = new[]
{
new Polyline(new[] { new Vector3(2, -1), new Vector3(2, 9) }),
new Polyline(new[] { new Vector3(-1, 4), new Vector3(11, 4) }),
new Polyline(new[] { new Vector3(6, -1), new Vector3(6, 9) })
};
var graph = HalfEdgeGraph2d.Construct(new[] { polygon }, cutters);
var regions = graph.Polygonize();
11.3.3 从线段集合构造
var lines = new[]
{
new Line(new Vector3(0, 0), new Vector3(5, 0)),
new Line(new Vector3(5, 0), new Vector3(5, 5)),
new Line(new Vector3(5, 5), new Vector3(0, 5)),
new Line(new Vector3(0, 5), new Vector3(0, 0)),
};
// bothWays = false(默认):每个线段创建一个半边
var graph = HalfEdgeGraph2d.Construct(lines);
var polygons = graph.Polygonize();
// bothWays = true:每个线段创建两个反向半边
var graphBi = HalfEdgeGraph2d.Construct(lines, bothWays: true);
11.3.4 Polygonize 算法
Polygonize() 方法遍历半边图,按”最大逆时针角”策略选择下一条边:
- 从任意有剩余半边的顶点出发
- 选取起始半边,沿其方向到达下一顶点
- 在下一顶点处,从未使用的出边中选择与”来边反向”形成最大逆时针角的边
- 重复直到回到起点(形成封闭环)或遇到死胡同
- 移除已遍历的半边,继续下一轮
// 可选:传入谓词过滤特定的边
var polygons = graph.Polygonize(
predicate: (tag) => tag == null || tag != 999, // 忽略 tag=999 的边
normal: Vector3.ZAxis // 指定遍历平面的法线
);
11.3.5 实战:从剖面交线重建多边形
// 场景:一个矩形楼板被两条管沟槽切割
// 需要重建每个子区域的多边形以计算面积
var floor = Polygon.Rectangle(new Vector3(0, 0), new Vector3(20, 15));
// 管沟 A:沿 X=5 从 Y=2 到 Y=13
var trenchA = new Polyline(new[]
{
new Vector3(5, 2),
new Vector3(5, 13)
});
// 管沟 B:沿 X=14 从 Y=4 到 Y=11
var trenchB = new Polyline(new[]
{
new Vector3(14, 4),
new Vector3(14, 11)
});
// 构建半边图并提取子区域
var graph = HalfEdgeGraph2d.Construct(new[] { floor },
new[] { trenchA, trenchB });
var subRegions = graph.Polygonize();
Console.WriteLine($"楼板被分为 {subRegions.Count} 个子区域:");
double totalArea = 0;
foreach (var region in subRegions)
{
var area = region.Area();
totalArea += area;
Console.WriteLine($" 区域面积: {area:F2} m², 周长: {region.Length():F2} m");
}
Console.WriteLine($"总面积验证: {totalArea:F2} m² (应等于原楼板面积 {floor.Area():F2} m²)");
11.4 CellComplex:三维拓扑复形
11.4.1 核心概念
CellComplex 是一个完整的三维非流形拓扑结构,用于表示建筑空间的三维关系。它继承自 Element,可以直接添加到 Model 中。
拓扑层级从高到低:
CellComplex (顶层容器)
├── Cell (体) ← 三维封闭挤压体(如一个房间单元)
│ ├── Face (面) ← 二维多边形面(如一面墙、楼板)
│ │ ├── DirectedEdge (有向边) ← 带方向的面边界段
│ │ │ └── Edge (边) ← 无向边,连接两个顶点
│ │ └── ...
│ └── ...
└── Vertex (顶点) ← 三维空间中的唯一坐标点
关键类的职责:
| 类 | 继承自 | 核心属性 |
|---|---|---|
CellComplex |
Element |
_vertices, _edges, _directedEdges, _faces, _cells |
Cell |
ChildBase<Cell, Extrude> |
FaceIds, BottomFaceId, TopFaceId |
Face |
ChildBase<Face, Polygon> |
DirectedEdgeIds, Cells (共享此面的 Cell 集合) |
Edge |
EdgeBase<Edge> |
StartVertexId, EndVertexId |
DirectedEdge |
EdgeBase<DirectedEdge> |
EdgeId(引用无向边), Faces |
Vertex |
VertexBase<Vertex> |
Value(坐标点), Edges |
DirectedEdge 是 Edge 的有向版本——同一条无向边最多有两个 DirectedEdge(正反方向)。引入 DirectedEdge 是为了在遍历 Face 边界时保持一致的环绕方向。
Orientation 是 Vertex 的变体,用于存储方向向量(UV 轴、法线),而非位置坐标。
11.4.2 创建 Cell:最简单的单元
using Elements;
using Elements.Geometry;
using Elements.Spatial.CellComplex;
var complex = new CellComplex(name: "建筑空间拓扑");
// 定义一个矩形房间轮廓
var roomOutline = Polygon.Rectangle(
new Vector3(0, 0),
new Vector3(6, 4)
);
// 创建 U/V 方向网格(用于 Face 内部的方向性存储)
var uGrid = new Grid1d(new Line(Vector3.Origin, new Vector3(6, 0, 0)));
var vGrid = new Grid1d(new Line(Vector3.Origin, new Vector3(0, 4, 0)));
// 添加一个单元:底面 + 3m 高度
var cell = complex.AddCell(roomOutline, height: 3.0, elevation: 0.0,
uGrid: uGrid, vGrid: vGrid);
Console.WriteLine($"单元 ID: {cell.Id}");
Console.WriteLine($"面数量: {cell.GetFaces().Count}"); // 6 个面
Console.WriteLine($"底面 ID: {cell.BottomFaceId}");
Console.WriteLine($"顶面 ID: {cell.TopFaceId}");
11.4.3 遍历拓扑关系
CellComplex 实现了完整的拓扑遍历 API:
// 从 Cell 出发
var cell = complex.GetCell(1);
var faces = cell.GetFaces(); // 该单元的所有面
var edges = cell.GetEdges(); // 该单元的所有边(去重)
var vertices = cell.GetVertices(); // 该单元的所有顶点(去重)
var neighbors = cell.GetNeighbors(); // 相邻单元(共享面的单元)
// 从 Face 出发
var face = cell.GetFaces().First();
var faceEdges = face.GetEdges(); // 面的边
var faceVertices = face.GetVertices(); // 面的顶点(按环绕顺序)
var faceCells = face.GetCells(); // 共享此面的所有单元
var faceNeighbors = face.GetNeighbors(); // 相邻面(共享边的面)
// 从 Edge 出发
var edge = face.GetEdges().First();
var edgeDirectedEdges = edge.GetDirectedEdges(); // 关联的有向边
var edgeFaces = edge.GetFaces(); // 共享此边的面
// 最近查询
var closestVertex = complex.GetClosestVertex(new Vector3(3, 2, 1.5));
var closestFace = complex.GetClosestFace(new Vector3(8, 1, 2));
var closestCell = complex.GetClosestCell(new Vector3(-1, 2, 3));
11.4.4 空间遍历算法
CellComplex 内置了基于距离梯度的邻居遍历:
// 朝向目标点遍历相邻 Cell —— 自动找到"方向"
var startCell = complex.GetCell(1);
var targetPoint = new Vector3(20, 15, 3);
var path = startCell.TraverseNeighbors(
targetPoint,
completedRadius: 0.5 // 距离目标 0.5m 内停止
);
Console.WriteLine($"从 Cell {startCell.Id} 遍历到目标附近共经过 {path.Count} 个单元");
类似地,Face.TraverseNeighbors() 也支持面间遍历。
11.4.5 几何获取
每个拓扑层级都可提取其几何表示:
// Cell → Extrude (SolidOperation)
var cellGeom = cell.GetGeometry(); // Extrude 对象
// Face → Polygon
var faceGeom = face.GetGeometry();
// 距离计算
var distToCell = cell.DistanceTo(somePoint);
var distToFace = face.DistanceTo(somePoint);
11.4.6 实战:多层建筑的空间拓扑
var complex = new CellComplex(name: "两层办公建筑");
// 定义标准房间尺寸
var smallRoom = Polygon.Rectangle(new Vector3(0, 0), new Vector3(4, 4));
var largeRoom = Polygon.Rectangle(new Vector3(0, 0), new Vector3(6, 5));
// 第一层:三个房间
var room1A = complex.AddCell(smallRoom, height: 3.0, elevation: 0.0);
var room1B = complex.AddCell(
smallRoom.TransformedPolygon(new Transform(4, 0, 0)),
height: 3.0, elevation: 0.0);
var room1C = complex.AddCell(
largeRoom.TransformedPolygon(new Transform(0, 4, 0)),
height: 3.0, elevation: 0.0);
// 第二层(同样的轮廓,不同标高)
var room2A = complex.AddCell(smallRoom, height: 3.0, elevation: 3.0);
var room2B = complex.AddCell(
smallRoom.TransformedPolygon(new Transform(4, 0, 0)),
height: 3.0, elevation: 3.0);
Console.WriteLine($"空间总数: {complex.GetCells().Count}");
// 查找相邻房间
foreach (var cell in complex.GetCells())
{
var neighbors = cell.GetNeighbors();
Console.WriteLine($"Cell {cell.Id}: {neighbors.Count} 个相邻单元");
}
// 添加为 Element 到 Model
var model = new Model();
model.AddElement(complex);
model.ToGlTF("cell_complex.glb");
11.5 AdaptiveGrid:自适应网格
11.5.1 核心概念
AdaptiveGrid 是一个动态细分的三维图结构,专为机电管线(MEP)寻路设计。它不同于 Grid2d 的规则矩形网格——AdaptiveGrid 的顶点和边可以在三维空间中任意分布,通过”按需插入”的方式逐步构建。
核心构成:
| 类型 | 角色 |
|---|---|
Vertex |
三维顶点,存储 (Id, Point, Edges) |
Edge |
连接两个顶点的无向边,存储 (Id, StartId, EndId) |
AdaptiveGrid |
容器类,管理顶点/边的增删改查 |
关键设计:
- 坐标去重:
AddVertex()通过内部 XYZ 坐标查找表自动合并容差范围内的重复顶点 - 边去重:
AddInsertEdge()通过哈希表确保同一对顶点之间只有一条边 - 自动切割:
AddCutEdge()在插入新边时自动检测与已有边的交点并分割 - 快照/恢复:
SnapshotEdgesOnPlane()/InsertSnapshot()支持在不同标高间复制图结构
11.5.2 构造与添加顶点
using Elements;
using Elements.Geometry;
using Elements.Spatial.AdaptiveGrid;
var grid = new AdaptiveGrid();
// 添加单个顶点
var v1 = grid.AddVertex(new Vector3(0, 0, 0));
var v2 = grid.AddVertex(new Vector3(5, 0, 0));
// 重复坐标自动合并——返回已有顶点
var v1Again = grid.AddVertex(new Vector3(0, 0, 0));
Console.WriteLine(v1.Id == v1Again.Id); // True
11.5.3 添加边
// 方式 1:简单插入(不做切割)
var edge1 = grid.AddEdge(v1.Id, v2.Id, cut: false);
// 方式 2:带切割的插入(默认)——如有交点,自动分割两边
var edge2 = grid.AddEdge(
new Vector3(2, 3, 0),
new Vector3(2, -2, 0),
cut: true
);
// 方式 3:用顶点对象
var v3 = grid.AddVertex(new Vector3(10, 0, 0));
grid.AddEdge(v2, v3, cut: true);
11.5.4 从区域生成网格
从包围盒生成:
var bbox = new BBox3(
new Vector3(0, 0, 0),
new Vector3(20, 15, 4)
);
// 关键分割点(管井位置、结构柱位等)
var keyPoints = new List<Vector3>
{
new Vector3(5, 3, 0),
new Vector3(5, 3, 4),
new Vector3(15, 10, 0),
new Vector3(15, 10, 4),
};
grid.AddFromBbox(bbox, keyPoints);
Console.WriteLine($"顶点数: {grid.GetVertices().Count}");
Console.WriteLine($"边数: {grid.GetEdges().Count}");
从多边形挤出生成:
var region = Polygon.Rectangle(new Vector3(0, 0), new Vector3(10, 8));
var grid = new AdaptiveGrid();
grid.AddFromExtrude(
boundingPolygon: region,
extrusionAxis: Vector3.ZAxis,
distance: 3.5,
keyPoints: new List<Vector3> { new Vector3(3, 4, 1.5) }
);
从单层多边形生成:
var floorPolygon = Polygon.LShape(15, 12, 5, 4);
var grid = new AdaptiveGrid();
var addedEdges = grid.AddFromPolygon(
floorPolygon,
new[] { new Vector3(3, 3, 0), new Vector3(10, 8, 0) }
);
11.5.5 添加障碍物(Obstacles)
障碍物定义了管线不应穿过的区域。Obstacle 有四种工厂方法:
// 从柱子创建障碍
var column = new Column(new Vector3(5, 5, 0), 4.0,
new Profile(Polygon.Rectangle(0.4, 0.4)));
var obstacle1 = Obstacle.FromColumn(column, offset: 0.15);
// 从墙体创建障碍
var wall = new StandardWall(
new Line(new Vector3(3, 0, 0), new Vector3(3, 10, 0)),
0.2, 3.0);
var obstacle2 = Obstacle.FromWall(wall, offset: 0.1);
// 从包围盒创建障碍
var box = new BBox3(
new Vector3(8, 2, 0),
new Vector3(9, 8, 3));
var obstacle3 = Obstacle.FromBBox(box, offset: 0.05);
// 从线段创建障碍
var pipeLine = new Line(new Vector3(12, 3, 0), new Vector3(12, 7, 3));
var obstacle4 = Obstacle.FromLine(pipeLine, offset: 0.2);
将障碍应用于网格:
// 带周长边的障碍:切割碰到的边并在障碍周围生成巡游边
var obstacle = Obstacle.FromColumn(
column, offset: 0.15,
addPerimeterEdges: true);
grid.SubtractObstacle(obstacle);
// 与 obstacle 相交的边被移除或剪短
// 障碍周围生成新的边(AddPerimeterEdges=true)
11.5.6 顶点连线策略
AddVertices() 支持六种插入模式,从简到繁:
| 模式 | 说明 |
|---|---|
Insert |
仅插入顶点,不连接 |
Connect |
按顺序连接顶点 |
ConnectAndSelfIntersect |
连接并检测新边之间的自相交 |
ConnectAndCut |
连接并与已有边相交切割 |
ConnectCutAndExtend |
连接+切割+端点延伸到最近的已有边 |
// 最简单的连线:连接 A→B→C
var path = new[] {
new Vector3(1, 1, 2),
new Vector3(5, 1, 2),
new Vector3(5, 5, 2)
};
var vertices = grid.AddVertices(path, VerticesInsertionMethod.ConnectAndCut);
11.5.7 实战:用 AdaptiveGrid 创建机电管线路径网络
using Elements;
using Elements.Geometry;
using Elements.Spatial.AdaptiveGrid;
var model = new Model();
// 1. 创建自适应网格
var grid = new AdaptiveGrid();
// 2. 按楼层分区填充网格
var floor1 = Polygon.Rectangle(new Vector3(0, 0), new Vector3(30, 20));
var floor2 = Polygon.Rectangle(new Vector3(0, 0), new Vector3(30, 20));
grid.AddFromPolygon(floor1, new List<Vector3> {
new Vector3(5, 5, 0), new Vector3(15, 10, 0), new Vector3(25, 5, 0)
});
// 复制一层网格到二层(标高 4.0m)
var snapshot = grid.SnapshotEdgesOnPlane(
new Plane(new Vector3(0, 0, 0), Vector3.ZAxis));
grid.InsertSnapshot(snapshot, new Transform(0, 0, 4.0));
// 3. 添加结构柱障碍
var columnPositions = new[]
{
new Vector3(5, 5, 0), new Vector3(5, 15, 0),
new Vector3(15, 15, 0), new Vector3(15, 5, 0),
new Vector3(25, 5, 0), new Vector3(25, 15, 0),
};
foreach (var pos in columnPositions)
{
var column = new Column(pos, 8.0,
new Profile(Polygon.Rectangle(0.5, 0.5)));
var obstacle = Obstacle.FromColumn(
column, offset: 0.2,
addPerimeterEdges: true);
grid.SubtractObstacle(obstacle);
}
// 4. 添加穿越楼层的竖井
var shaft = grid.AddVertices(
new[] {
new Vector3(10, 10, 0),
new Vector3(10, 10, 4.0)
},
VerticesInsertionMethod.ConnectAndCut
);
Console.WriteLine($"网格顶点总数: {grid.GetVertices().Count}");
Console.WriteLine($"网格边总数: {grid.GetEdges().Count}");
11.6 AdaptiveGraphRouting:自适应图路由
11.6.1 核心概念
AdaptiveGraphRouting 是在 AdaptiveGrid 上运行的路径搜索引擎,用于寻找机电管线的最优路径。它基于 Dijkstra 最短路径算法的扩展,支持:
- 权重因子:每条边关联一个成本因子(
EdgeInfo.Factor),影响路由选择 - 提示线(HintLines):吸引路由向特定线路靠拢(如走廊、管沟)
- 权重修改器(WeightModifier):自定义边权重逻辑
- 转弯惩罚(TurnCost):路由改变方向时增加额外成本
- 角度限制(SupportedAngles):约束路由只能按特定角度转弯
- 多分支生成树(SpanningTree):多个入口合并到主干出口
11.6.2 路由配置
var config = new RoutingConfiguration(
turnCost: 0.5, // 每次转弯的成本惩罚
supportedAngles: new List<double> { 0, 45, 90 } // 仅允许 0°、45°、90° 转弯
);
var routing = new AdaptiveGraphRouting(grid, config);
11.6.3 提示线(RoutingHintLine)
提示线是路由的”引力场”——靠近提示线的边有更低的成本因子,路由会优先选择它们:
// 2D 提示线:仅影响 XY 平面(忽略 Z),适用于水平管线走廊
var corridorLine = new Polyline(new[]
{
new Vector3(2, 3, 0),
new Vector3(28, 3, 0)
});
var hint2D = new RoutingHintLine(
corridorLine,
factor: 0.3, // 成本乘以 0.3(大幅折扣)
influence: 1.5, // 影响半径 1.5m
userDefined: true, // 用户定义(高优先级)
is2D: true
);
// 3D 提示线:影响完整三维空间,适用于竖井
var shaftLine = new Polyline(new[]
{
new Vector3(10, 10, 0),
new Vector3(10, 10, 4.5)
});
var hint3D = new RoutingHintLine(
shaftLine,
factor: 0.5,
influence: 0.8,
userDefined: true,
is2D: false
);
11.6.4 权重修改器(WeightModifier)
WeightModifier 提供比提示线更灵活的边权重控制。多个修改器按组聚合(同组取最小因子,各组相乘):
// 禁止穿越特定标高
var noHighZone = new WeightModifier(
"禁止高空区域",
condition: (start, end) =>
start.Point.Z > 6.0 || end.Point.Z > 6.0,
factor: 1000.0, // 极高成本 → 实际禁止
group: "elevation"
);
// 偏好水平管道(减少竖直段的权重)
var preferHorizontal = new WeightModifier(
"偏好水平走向",
condition: (start, end) =>
Math.Abs(start.Point.Z - end.Point.Z) < 0.01,
factor: 0.7,
group: "direction"
);
routing.SetWeightModifier(noHighZone);
routing.SetWeightModifier(preferHorizontal);
11.6.5 简单网络路由
为每个入口独立寻找通往任意出口的最优路径:
// 入口(设备接口位置)
var inletVertices = new List<RoutingVertex>
{
new RoutingVertex(id: v1.Id, isolationRadius: 0.5),
new RoutingVertex(id: v2.Id, isolationRadius: 0.5),
};
// 出口(主管道连接点)
var exitVertexIds = new List<ulong> { vMainTrunk.Id };
// 提示线
var hints = new List<RoutingHintLine> { hint2D, hint3D };
// 执行路由
var tree = routing.BuildSimpleNetwork(
inletVertices,
exitVertexIds,
hints
);
// 遍历路径树
foreach (var (leafId, node) in tree)
{
var pathVertices = new List<Vector3>();
var current = node;
while (current != null)
{
pathVertices.Add(grid.GetVertex(current.Id).Point);
current = current.Trunk;
}
Console.WriteLine($"从 {leafId} 到出口的路径包含 {pathVertices.Count} 个节点");
}
11.6.6 生成树路由(合并主干)
多个入口的路由先各自寻路,然后合并到共同的主干,减少重复管线:
var tree = routing.BuildSpanningTree(
leafVertices: inletVertices,
trunkVertex: vMainTrunk.Id,
hintLines: hints,
order: TreeOrder.ClosestToFurthest // 从近到远依次接入
);
TreeOrder 决定接入顺序:
| 值 | 策略 |
|---|---|
ClosestToFurthest |
距离出口最近的入口先接入(优先形成主干) |
FurthestToClosest |
最远的先接入 |
11.6.7 可视化路由结果
var elements = routing.RenderElements(
hintLines: hints,
splitPoints: keyPoints
);
foreach (var el in elements)
{
model.AddElement(el);
}
model.ToGlTF("routing_visualization.glb");
渲染按边类型着色:
| 颜色 | 含义 |
|---|---|
| 蓝色 | 正常边(Factor = 1.0) |
| 绿色(翡翠) | 打折边(Factor < 1.0) |
| 紫色 | 昂贵边(Factor > 1.0) |
| 绿色 | 用户提示边 |
| 黄色 | 用户提示但高成本边 |
| 橙色 | 偏移/隐藏提示边 |
11.6.8 实战:机电管线自动寻路
using Elements;
using Elements.Geometry;
using Elements.Spatial.AdaptiveGrid;
var model = new Model();
// === 步骤 1:创建自适应网格 ===
var grid = new AdaptiveGrid();
var buildingFootprint = Polygon.Rectangle(
new Vector3(0, 0), new Vector3(30, 20));
grid.AddFromPolygon(buildingFootprint, new List<Vector3> {
new Vector3(10, 0, 0), new Vector3(20, 0, 0),
new Vector3(0, 10, 0), new Vector3(0, 15, 0),
});
// === 步骤 2:定义障碍 ===
// 结构柱
var columns = new (double x, double y)[]
{
(5, 5), (5, 15), (15, 15), (15, 5), (25, 5), (25, 15)
};
foreach (var (x, y) in columns)
{
var col = new Column(new Vector3(x, y, 0), 4.0,
new Profile(Polygon.Rectangle(0.5, 0.5)));
grid.SubtractObstacle(
Obstacle.FromColumn(col, offset: 0.2, addPerimeterEdges: true));
}
// === 步骤 3:定义提示线 ===
// 沿走廊的偏好路径(X=10 到 X=20,Y=10 处)
var mainCorridor = new RoutingHintLine(
new Polyline(new[] {
new Vector3(0, 10, 0),
new Vector3(30, 10, 0)
}),
factor: 0.4, influence: 2.0,
userDefined: true, is2D: true
);
// 沿次要走廊(Y=5 到 Y=15,X=15 处)
var secondaryCorridor = new RoutingHintLine(
new Polyline(new[] {
new Vector3(15, 0, 0),
new Vector3(15, 20, 0)
}),
factor: 0.6, influence: 1.5,
userDefined: true, is2D: true
);
// === 步骤 4:执行路由 ===
var config = new RoutingConfiguration(turnCost: 1.0);
var routing = new AdaptiveGraphRouting(grid, config);
// 接入口(设备位置)
var inletA = grid.AddVertex(new Vector3(2, 3, 0));
var inletB = grid.AddVertex(new Vector3(28, 17, 0));
// 出口(主管井位置)
var trunk = grid.AddVertex(new Vector3(10, 10, 0));
var inlets = new List<RoutingVertex>
{
new RoutingVertex(inletA.Id, isolationRadius: 1.0),
new RoutingVertex(inletB.Id, isolationRadius: 1.0),
};
var hints = new List<RoutingHintLine> { mainCorridor, secondaryCorridor };
var tree = routing.BuildSpanningTree(
inlets, trunk.Id, hints, TreeOrder.ClosestToFurthest);
Console.WriteLine($"路由树包含 {tree.Count} 个节点");
// === 步骤 5:提取路径并创建管道几何 ===
foreach (var (leafId, node) in tree)
{
var pathIds = new List<ulong>();
var current = node;
while (current != null)
{
pathIds.Add(current.Id);
current = current.Trunk;
}
Console.WriteLine($"\n从入口 {leafId} 到出口的路径:");
foreach (var vid in pathIds)
{
var v = grid.GetVertex(vid);
Console.WriteLine($" → ({v.Point.X:F1}, {v.Point.Y:F1}, {v.Point.Z:F1})");
}
}
11.7 本章小结
本章介绍了 Elements 空间子系统的六个核心组件,覆盖了从简单的一维轴线到复杂的三维拓扑与寻路的完整工具链:
- Grid1d 是幕墙分格、柱距排布等一维划分任务的基础工具,支持多种分割和模式化策略
- Grid2d 将 U/V 两个方向组合为二维网格,是柱网布局、幕墙面板排列的主力
- HalfEdgeGraph2d 解决剖面交线重建多边形的拓扑难题,是几何分析的关键工具
- CellComplex 提供了完整的三维拓扑结构(Cell/Face/Edge/Vertex/DirectedEdge),支持空间关系和邻居遍历
- AdaptiveGrid 是动态的三维图结构,可按需插入顶点和边,支持障碍物裁剪
- AdaptiveGraphRouting 基于 Dijkstra 算法,通过提示线和权重修改器引导最优管线路径
这些工具共同构成了 Elements 作为”BIM 编程引擎”的空间智能层,使自动化建筑规则检查、管线排布优化等高级工作流成为可能。