znlgis 博客

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

第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)可以继续递归细分,形成层次化的网格结构。其核心属性是 DomainDomain1d 类型)——描述该网格覆盖的数值范围 [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 类型:通过 DivideByCountDivideByApproximateLengthDivideByFixedLength 等方法创建的子格。这些方法产生的子格 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() 方法遍历半边图,按”最大逆时针角”策略选择下一条边:

  1. 从任意有剩余半边的顶点出发
  2. 选取起始半边,沿其方向到达下一顶点
  3. 在下一顶点处,从未使用的出边中选择与”来边反向”形成最大逆时针角的边
  4. 重复直到回到起点(形成封闭环)或遇到死胡同
  5. 移除已遍历的半边,继续下一轮
// 可选:传入谓词过滤特定的边
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

DirectedEdgeEdge 的有向版本——同一条无向边最多有两个 DirectedEdge(正反方向)。引入 DirectedEdge 是为了在遍历 Face 边界时保持一致的环绕方向。

OrientationVertex 的变体,用于存储方向向量(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 编程引擎”的空间智能层,使自动化建筑规则检查、管线排布优化等高级工作流成为可能。


← 上一章 目录 下一章 →