znlgis 博客

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

第16章:实战案例——综合建模项目

前面各章已系统讲解 Elements 的几何系统、建筑元素、CSG 布尔运算、材质渲染、序列化互操作、空间搜索与 MEP 机电系统。本章把这些知识整合为一个完整的实战项目:从零构建一座两层小型办公楼

本项目涵盖 BIM 建模的全流程——结构框架、建筑围护、楼梯、材质、空间定义、机电管线示意,以及多格式导出。完整的项目代码按专业模块拆分,结构清晰,可直接作为实际项目的起点。

16.1 项目需求

16.1.1 建筑概况

项目 参数
建筑类型 两层小型办公楼
平面尺寸 18m × 12m(矩形平面)
层高 首层 3.6m,二层 3.3m
柱网 6m × 6m 网格(3 × 2 跨)
总高度 7.5m(含 0.6m 女儿墙)

16.1.2 需要创建的模型组件

  • 结构框架:混凝土柱(400×400)、主梁(300×600)、次梁(250×400)、楼板(150mm 厚)
  • 建筑围护:外墙(200mm 厚砌块墙)、内隔墙(100mm 厚轻质隔墙)、幕墙面板(首层南立面)
  • 楼梯:双跑楼梯,位于建筑北侧中部
  • 屋顶:平屋顶 + 女儿墙
  • 门窗开洞:外墙门洞(首层)、窗洞(各层)
  • 材质:混凝土、钢材、玻璃、砌块、木材
  • 空间定义:办公室、走廊、卫生间、楼梯间
  • 机电示意:给排水立管与水平支管

16.2 项目架构设计

在正式编码之前,先设计项目的代码组织方式。按专业领域拆分,每个模块负责一组相关元素的创建:

OfficeBuilding/
├── Program.cs                 入口:组装模型并导出
├── ModelBuilder.cs            模型构建器(协调各模块)
├── Structural/
│   ├── StructuralFrame.cs     柱网、梁、楼板
│   └── StairsBuilder.cs       楼梯(梯段、平台、栏杆)
├── Architectural/
│   ├── WallsBuilder.cs        内外墙
│   ├── CurtainWallBuilder.cs  幕墙面板
│   └── OpeningsBuilder.cs     门窗开洞
├── MEP/
│   └── PlumbingLayout.cs      给排水示意管线
├── Materials/
│   └── MaterialLibrary.cs     材质库(单例)
├── Spaces/
│   └── SpaceLayout.cs         房间空间定义
└── Export/
    └── ExportPipeline.cs      多格式导出(glTF/IFC/SVG)

16.2.1 ModelBuilder:核心协调器

ModelBuilder 是整个项目的入口协调器,负责按正确顺序调用各模块,以及管理共享的 Model 实例和全局参数:

public class ModelBuilder
{
    public Model Model { get; } = new Model();
    public BuildingParams Params { get; }

    public ModelBuilder()
    {
        Params = new BuildingParams
        {
            LengthX = 18.0,
            LengthY = 12.0,
            GridSpacingX = 6.0,
            GridSpacingY = 6.0,
            GroundFloorHeight = 3.6,
            UpperFloorHeight = 3.3,
            ParapetHeight = 0.6,
            ColumnSize = (0.4, 0.4),
            MainBeamSize = (0.3, 0.6),
            SecondaryBeamSize = (0.25, 0.4),
            SlabThickness = 0.15,
            ExteriorWallThickness = 0.2,
            InteriorWallThickness = 0.1,
        };
    }

    public void Build()
    {
        var frame = new StructuralFrame(this);
        frame.CreateColumns();
        frame.CreateBeams();
        frame.CreateSlabs();

        var walls = new WallsBuilder(this);
        walls.CreateExteriorWalls();
        walls.CreateInteriorWalls();

        var curtainWall = new CurtainWallBuilder(this);
        curtainWall.BuildFacade();

        var openings = new OpeningsBuilder(this);
        openings.CreateOpenings();

        var stairs = new StairsBuilder(this);
        stairs.BuildStairs();

        var spaces = new SpaceLayout(this);
        spaces.DefineSpaces();

        var plumbing = new PlumbingLayout(this);
        plumbing.CreateRisers();
    }
}

public class BuildingParams
{
    public double LengthX { get; set; }
    public double LengthY { get; set; }
    public double GridSpacingX { get; set; }
    public double GridSpacingY { get; set; }
    public double GroundFloorHeight { get; set; }
    public double UpperFloorHeight { get; set; }
    public double ParapetHeight { get; set; }
    public (double w, double h) ColumnSize { get; set; }
    public (double w, double h) MainBeamSize { get; set; }
    public (double w, double h) SecondaryBeamSize { get; set; }
    public double SlabThickness { get; set; }
    public double ExteriorWallThickness { get; set; }
    public double InteriorWallThickness { get; set; }
    public double TotalHeight => GroundFloorHeight + UpperFloorHeight + ParapetHeight;
}

16.2.2 入口程序

// Program.cs
class Program
{
    static void Main(string[] args)
    {
        var builder = new ModelBuilder();
        builder.Build();

        Console.WriteLine($"模型构建完成,共 {builder.Model.Elements.Count} 个元素");

        var exporter = new ExportPipeline(builder.Model);
        exporter.ExportAll("output/OfficeBuilding");
    }
}

16.3 结构框架创建

结构框架是建筑的骨架。使用 Grid2d 定义柱网定位,然后依次创建柱、梁和楼板。

16.3.1 柱网定义与柱创建

Grid2d 提供规则的网格定位系统,非常适合柱网的参数化定义:

using Elements;
using Elements.Geometry;
using Elements.Spatial;

public class StructuralFrame
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public StructuralFrame(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void CreateColumns()
    {
        // 定义 X 方向网格:3 跨 × 6m = 18m
        var grid1dX = new Grid1d(_p.LengthX);
        grid1dX.SplitAtOffsets(new[]
        {
            _p.GridSpacingX,
            _p.GridSpacingX * 2,
            _p.GridSpacingX * 3
        });

        // 定义 Y 方向网格:2 跨 × 6m = 12m
        var grid1dY = new Grid1d(_p.LengthY);
        grid1dY.SplitAtOffsets(new[]
        {
            _p.GridSpacingY,
            _p.GridSpacingY * 2
        });

        // 组合为二维网格
        var grid2d = new Grid2d(grid1dX, grid1dY);

        var columnProfile = new Profile(
            Polygon.Rectangle(_p.ColumnSize.w, _p.ColumnSize.h)
        );

        var concrete = MaterialLibrary.Concrete;

        // 在网格的每个节点处创建柱(共 4×3 = 12 根柱)
        int colIndex = 1;
        foreach (var cell in grid2d.GetCells())
        {
            // 取单元格的左下角作为柱位
            var bottomLeft = cell.GetCellGeometry()[0].Start;

            // 首层柱:地面到二层楼板底
            var gfColumn = new Column(
                bottomLeft,
                _p.GroundFloorHeight,
                columnProfile
            )
            {
                Name = $"柱-GF-{colIndex}",
                Material = concrete
            };

            // 二层柱:二层楼板面到屋顶
            var ufColumn = new Column(
                new Vector3(bottomLeft.X, bottomLeft.Y, _p.GroundFloorHeight),
                _p.UpperFloorHeight,
                columnProfile
            )
            {
                Name = $"柱-UF-{colIndex}",
                Material = concrete
            };

            _model.AddElement(gfColumn);
            _model.AddElement(ufColumn);
            colIndex++;
        }
    }
}

16.3.2 梁创建

梁沿柱网轴线布置。主梁沿 Y 方向(横向框架),次梁沿 X 方向:

public void CreateBeams()
{
    var mainBeamProfile = new Profile(
        Polygon.Rectangle(_p.MainBeamSize.w, _p.MainBeamSize.h)
    );
    var secBeamProfile = new Profile(
        Polygon.Rectangle(_p.SecondaryBeamSize.w, _p.SecondaryBeamSize.h)
    );
    var steel = MaterialLibrary.Steel;

    // 每层楼板标高处创建梁格
    foreach (var elevation in new[] { _p.GroundFloorHeight, _p.GroundFloorHeight + _p.UpperFloorHeight })
    {
        // Y 方向主梁:沿 X 方向的每条柱线布置
        for (double x = 0; x <= _p.LengthX; x += _p.GridSpacingX)
        {
            var line = new Line(
                new Vector3(x, 0, elevation),
                new Vector3(x, _p.LengthY, elevation)
            );
            var beam = new Beam(line, mainBeamProfile)
            {
                Name = $"主梁-X{x}-Z{elevation}",
                Material = steel
            };
            _model.AddElement(beam);
        }

        // X 方向次梁:沿 Y 方向的每条柱线布置
        for (double y = 0; y <= _p.LengthY; y += _p.GridSpacingY)
        {
            var line = new Line(
                new Vector3(0, y, elevation),
                new Vector3(_p.LengthX, y, elevation)
            );
            var beam = new Beam(line, secBeamProfile)
            {
                Name = $"次梁-Y{y}-Z{elevation}",
                Material = steel
            };
            _model.AddElement(beam);
        }
    }
}

16.3.3 楼板创建

楼板使用矩形轮廓,在每层梁顶标高处创建:

public void CreateSlabs()
{
    var concrete = MaterialLibrary.Concrete;
    var slabOutline = Polygon.Rectangle(_p.LengthX, _p.LengthY);

    // 二层楼板(标高 3.6m 处)
    var slab2f = new Floor(
        slabOutline,
        _p.SlabThickness,
        new Transform(0, 0, _p.GroundFloorHeight)
    )
    {
        Name = "二层楼板",
        Material = concrete
    };

    // 屋顶楼板(标高 6.9m 处)
    var roofSlab = new Floor(
        slabOutline,
        _p.SlabThickness,
        new Transform(0, 0, _p.GroundFloorHeight + _p.UpperFloorHeight)
    )
    {
        Name = "屋顶楼板",
        Material = concrete
    };

    _model.AddElement(slab2f);
    _model.AddElement(roofSlab);
}

Elements 中楼板通过 Transform 参数控制 Z 向标高,轮廓的 XY 坐标自动转换到指定位置。

16.4 建筑围护

建筑围护包括外墙、内隔墙、南立面幕墙,以及门洞和窗洞。开洞通过 Representation.SolidOperations 中添加 IsVoid = trueExtrude 来实现。

16.4.1 外墙

外墙沿建筑外轮廓布置,使用砌块材质,厚度 200mm:

using Elements;
using Elements.Geometry;

public class WallsBuilder
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public WallsBuilder(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void CreateExteriorWalls()
    {
        var extProfile = new Profile(
            Polygon.Rectangle(_p.ExteriorWallThickness, _p.TotalHeight)
        );
        var masonry = MaterialLibrary.Masonry;

        // 四面外墙:从原点逆时针围合
        var exteriorLines = new[]
        {
            new Line(new Vector3(0, 0, 0), new Vector3(_p.LengthX, 0, 0)),           // 南
            new Line(new Vector3(_p.LengthX, 0, 0), new Vector3(_p.LengthX, _p.LengthY, 0)), // 东
            new Line(new Vector3(_p.LengthX, _p.LengthY, 0), new Vector3(0, _p.LengthY, 0)), // 北
            new Line(new Vector3(0, _p.LengthY, 0), new Vector3(0, 0, 0)),           // 西
        };

        string[] wallNames = { "外墙-南", "外墙-东", "外墙-北", "外墙-西" };

        for (int i = 0; i < exteriorLines.Length; i++)
        {
            var wall = new Wall(exteriorLines[i], extProfile, _p.TotalHeight)
            {
                Name = wallNames[i],
                Material = masonry
            };
            _model.AddElement(wall);
        }
    }
}

16.4.2 内隔墙

内隔墙分隔办公空间,厚度 100mm。在 6m 柱距处沿 Y 方向、以及沿走廊中线沿 X 方向布置:

public void CreateInteriorWalls()
{
    var intProfile = new Profile(
        Polygon.Rectangle(_p.InteriorWallThickness, _p.TotalHeight)
    );
    var gypsum = MaterialLibrary.Gypsum;

    // 横向隔墙:在 X = 6m 和 X = 12m 处
    foreach (var x in new[] { _p.GridSpacingX, _p.GridSpacingX * 2 })
    {
        var line = new Line(
            new Vector3(x, 0, 0),
            new Vector3(x, _p.LengthY, 0)
        );
        var wall = new Wall(line, intProfile, _p.TotalHeight)
        {
            Name = $"内墙-横-X{x}",
            Material = gypsum
        };
        _model.AddElement(wall);
    }

    // 纵向走廊隔墙:Y = 6m 处(在建筑中部)
    var corridorWall = new Wall(
        new Line(
            new Vector3(0, _p.GridSpacingY, 0),
            new Vector3(_p.LengthX, _p.GridSpacingY, 0)
        ),
        intProfile,
        _p.TotalHeight
    )
    {
        Name = "内墙-走廊",
        Material = gypsum
    };
    _model.AddElement(corridorWall);
}

16.4.3 幕墙面板

南立面使用幕墙面板替代实墙。Panel 元素适合创建玻璃面板、幕墙单元等平面构件:

using Elements;
using Elements.Geometry;

public class CurtainWallBuilder
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public CurtainWallBuilder(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void BuildFacade()
    {
        var glass = MaterialLibrary.Glass;
        var panelWidth = 1.5;  // 每块面板宽 1.5m
        var panelCount = (int)(_p.LengthX / panelWidth);

        // 首层幕墙面板:从 Y=0 到 Y=0(南立面外墙位置)
        for (int i = 0; i < panelCount; i++)
        {
            double x = i * panelWidth;
            var panelOutline = Polygon.Rectangle(panelWidth, _p.GroundFloorHeight);

            // Panel 用矩形轮廓在 3D 空间中的平面放置
            var panel = new Panel(
                panelOutline,
                new Transform(x, 0, 0, 0, 0, 0)  // 放在南立面位置
            )
            {
                Name = $"幕墙面板-首层-{i + 1}",
                Material = glass
            };
            _model.AddElement(panel);
        }
    }
}

16.4.4 门洞与窗洞

门洞和窗洞通过 Opening 元素实现。Opening 是一个挖空元素,其 Representation.SolidOperations 中包含 IsVoid = true 的挤出操作:

using Elements;
using Elements.Geometry;
using Elements.Geometry.Solids;

public class OpeningsBuilder
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public OpeningsBuilder(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void CreateOpenings()
    {
        CreateDoorOpenings();
        CreateWindowOpenings();
    }

    private void CreateDoorOpenings()
    {
        // 首层南立面主入口(双开门,宽 1.8m × 高 2.4m)
        AddWallOpening(
            "门洞-主入口",
            _p.LengthX / 2 - 0.9, 0, 0.1,         // X 中心偏移,Y 在南墙,Z 底部
            1.8, 2.4                                 // 宽 × 高
        );

        // 首层北立面后勤入口
        AddWallOpening(
            "门洞-后门",
            _p.LengthX / 2 - 0.9, _p.LengthY - 0.1, 0.1,
            1.8, 2.4
        );

        // 二层走廊两端的门洞
        AddWallOpening(
            "门洞-东入口",
            _p.LengthX - 0.1, _p.LengthY / 2 - 0.45, 3.6,
            0.9, 2.1
        );
    }

    private void CreateWindowOpenings()
    {
        var windowWidth = 1.5;
        var windowHeight = 1.5;

        // 首层窗洞(南立面主入口两侧,各一扇)
        AddWallOpening("窗洞-GF-南-1", 1.5, 0.1, 1.2, windowWidth, windowHeight);
        AddWallOpening("窗洞-GF-南-2", _p.LengthX - 3.0, 0.1, 1.2, windowWidth, windowHeight);

        // 东立面窗洞(两层各两扇)
        for (int floor = 0; floor < 2; floor++)
        {
            double zBase = floor == 0 ? 1.2 : _p.GroundFloorHeight + 1.2;
            string label = floor == 0 ? "GF" : "UF";

            AddWallOpening(
                $"窗洞-{label}-东-1",
                _p.LengthX - 0.1, 2.0, zBase,
                windowWidth, windowHeight
            );
            AddWallOpening(
                $"窗洞-{label}-东-2",
                _p.LengthX - 0.1, _p.LengthY - 3.5, zBase,
                windowWidth, windowHeight
            );
        }

        // 北立面窗洞
        for (int floor = 0; floor < 2; floor++)
        {
            double zBase = floor == 0 ? 1.2 : _p.GroundFloorHeight + 1.2;
            string label = floor == 0 ? "GF" : "UF";

            AddWallOpening(
                $"窗洞-{label}-北-1",
                3.0, _p.LengthY - 0.1, zBase,
                windowWidth, windowHeight
            );
            AddWallOpening(
                $"窗洞-{label}-北-2",
                _p.LengthX - 4.5, _p.LengthY - 0.1, zBase,
                windowWidth, windowHeight
            );
        }
    }

    private void AddWallOpening(
        string name,
        double x, double y, double z,
        double width, double height)
    {
        var openingProfile = new Profile(Polygon.Rectangle(width, height));

        // 开洞方向:沿墙厚方向(此处统一用 Y 轴方向做穿墙开洞,实际项目中需根据墙的朝向调整方向)
        var opening = new Opening(
            openingProfile,
            x, y, z,
            width, height
        )
        {
            Name = name
        };

        // 在 Opening 的表示中添加 IsVoid 挤出
        var voidExtrude = new Extrude(
            openingProfile,
            width,              // 挤出深度 = 穿透墙厚
            Vector3.XAxis,       // 沿 X 方向穿透
            true                 // IsVoid = true
        );
        opening.Representation.SolidOperations.Add(voidExtrude);

        _model.AddElement(opening);
    }
}

开洞原理Opening 元素包含 IsVoid = trueSolidOperation。当 Model 执行 CSG 布尔运算时,挖空操作会从与之相交的所有实体元素中减去,从而在墙上形成真实的洞口。Opening 的尺寸应略大于门窗外框,以确保完全穿透墙体。

16.5 楼梯

双跑楼梯由梯段、中间平台和栏杆组成。使用 Beam 表示梯段斜梁,Extrude 创建踏步和平台板:

using Elements;
using Elements.Geometry;
using Elements.Geometry.Solids;

public class StairsBuilder
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    // 楼梯参数
    private const double StairWidth = 1.5;
    private const double RiserHeight = 0.15;
    private const double TreadDepth = 0.28;
    private const double LandingLength = 1.8;
    private const int StepsPerFlight = 12;

    public StairsBuilder(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void BuildStairs()
    {
        // 楼梯位于建筑北侧中部,X 方向居中
        double stairStartX = _p.LengthX / 2 - StairWidth / 2;
        double stairY = 2.0;  // 距北墙 2m

        BuildFlight(stairStartX, stairY, 0);                  // 首层 → 中间平台
        BuildIntermediateLanding(stairStartX, stairY);         // 中间平台(标高 1.8m)
        BuildFlight(stairStartX, stairY + LandingLength, 1);   // 中间平台 → 二层
        BuildLanding(stairStartX, stairY, _p.GroundFloorHeight);  // 二层楼面平台
        BuildRailings(stairStartX, stairY);
    }

    private void BuildFlight(double x, double y, int direction)
    {
        var steel = MaterialLibrary.Steel;
        double flightLength = StepsPerFlight * TreadDepth;

        // Y 方向起始位置随 direction 偏移
        double yStart = direction == 0 ? y : y + LandingLength;
        double zStart = direction == 0 ? 0 : StepsPerFlight * RiserHeight;

        // 梯段斜梁(矩形截面钢梁)
        var beamProfile = new Profile(Polygon.Rectangle(0.15, 0.35));
        var beamLine = new Line(
            new Vector3(x, yStart, zStart),
            new Vector3(x, yStart + flightLength, zStart + StepsPerFlight * RiserHeight)
        );
        var stringer = new Beam(beamLine, beamProfile)
        {
            Name = $"梯段斜梁-{(direction == 0 ? "下" : "上")}",
            Material = steel
        };
        _model.AddElement(stringer);

        // 逐级踏步
        for (int i = 0; i < StepsPerFlight; i++)
        {
            double stepZ = zStart + i * RiserHeight;
            double stepY = yStart + i * TreadDepth;

            var treadProfile = new Profile(Polygon.Rectangle(StairWidth, TreadDepth));
            var step = new Floor(
                Polygon.Rectangle(StairWidth, TreadDepth),
                RiserHeight,
                new Transform(x, stepY, stepZ)
            )
            {
                Name = $"踏步-{(direction == 0 ? "下" : "上")}-{i + 1}",
                Material = MaterialLibrary.Concrete
            };
            _model.AddElement(step);
        }
    }

    private void BuildIntermediateLanding(double x, double y)
    {
        double z = StepsPerFlight * RiserHeight;  // 中间平台标高

        var landing = new Floor(
            Polygon.Rectangle(StairWidth, LandingLength),
            0.15,
            new Transform(x, y + StepsPerFlight * TreadDepth, z)
        )
        {
            Name = "中间平台",
            Material = MaterialLibrary.Concrete
        };
        _model.AddElement(landing);
    }

    private void BuildLanding(double x, double y, double elevation)
    {
        var landing = new Floor(
            Polygon.Rectangle(StairWidth, LandingLength),
            0.15,
            new Transform(x, y + StepsPerFlight * TreadDepth + LandingLength, elevation)
        )
        {
            Name = "二层平台",
            Material = MaterialLibrary.Concrete
        };
        _model.AddElement(landing);
    }

    private void BuildRailings(double x, double y)
    {
        var steel = MaterialLibrary.Steel;
        var railingHeight = 0.9;
        var totalRun = 2 * StepsPerFlight * TreadDepth + LandingLength;

        // 立面栏杆:用细柱表示
        var postProfile = new Profile(Polygon.Rectangle(0.04, 0.04));
        double zBase = 0;

        for (double dist = 0; dist <= totalRun; dist += TreadDepth)
        {
            // 判断是梯段还是平台段
            bool isLowerFlight = dist <= StepsPerFlight * TreadDepth;
            bool isLanding = dist > StepsPerFlight * TreadDepth &&
                             dist <= StepsPerFlight * TreadDepth + LandingLength;

            double postZ;
            if (isLowerFlight)
            {
                postZ = (dist / TreadDepth) * RiserHeight;
            }
            else if (isLanding)
            {
                postZ = StepsPerFlight * RiserHeight;
            }
            else
            {
                double upperDist = dist - StepsPerFlight * TreadDepth - LandingLength;
                postZ = StepsPerFlight * RiserHeight + (upperDist / TreadDepth) * RiserHeight;
            }

            // 左侧栏杆柱
            var leftPost = new Column(
                new Vector3(x, y + dist, postZ),
                railingHeight,
                postProfile
            )
            {
                Name = $"栏杆柱-L-{dist:F1}",
                Material = steel
            };
            _model.AddElement(leftPost);

            // 右侧栏杆柱
            var rightPost = new Column(
                new Vector3(x + StairWidth, y + dist, postZ),
                railingHeight,
                postProfile
            )
            {
                Name = $"栏杆柱-R-{dist:F1}",
                Material = steel
            };
            _model.AddElement(rightPost);
        }
    }
}

16.6 材质赋予

将材质集中管理,各模块通过 MaterialLibrary 统一引用,确保一致性和可维护性:

using Elements;
using Elements.Geometry;

public static class MaterialLibrary
{
    public static Material Concrete { get; } = new Material("混凝土")
    {
        Color = new Color(0.7, 0.7, 0.7, 1.0),   // 浅灰色
        SpecularFactor = 0.1,
        GlossinessFactor = 0.2
    };

    public static Material Steel { get; } = new Material("钢材")
    {
        Color = new Color(0.55, 0.55, 0.6, 1.0),  // 钢灰色
        SpecularFactor = 0.5,
        GlossinessFactor = 0.6
    };

    public static Material Glass { get; } = new Material("玻璃")
    {
        Color = new Color(0.6, 0.8, 0.9, 0.4),     // 半透明浅蓝
        SpecularFactor = 0.9,
        GlossinessFactor = 0.95
    };

    public static Material Masonry { get; } = new Material("砌块")
    {
        Color = new Color(0.85, 0.78, 0.65, 1.0),  // 暖灰色砌块
        SpecularFactor = 0.05,
        GlossinessFactor = 0.1
    };

    public static Material Gypsum { get; } = new Material("石膏板")
    {
        Color = new Color(0.95, 0.95, 0.93, 1.0),  // 米白色
        SpecularFactor = 0.02,
        GlossinessFactor = 0.05
    };

    public static Material Wood { get; } = new Material("木材")
    {
        Color = new Color(0.55, 0.35, 0.15, 1.0),  // 棕色木纹
        SpecularFactor = 0.15,
        GlossinessFactor = 0.3
    };

    public static Material Copper { get; } = new Material("铜管")
    {
        Color = new Color(0.8, 0.5, 0.3, 1.0),     // 铜色
        SpecularFactor = 0.4,
        GlossinessFactor = 0.5
    };
}

材质命名规范:使用中文名称可读性好,在导出 IFC 时映射到标准材质分类。Material 元素的 Id 自动生成 GUID,在多个元素间共享同一个 Material 实例即可。

16.7 空间定义

建筑空间(房间)用 Space 元素定义。每个 Space 包含一个轮廓多边形、高度和位置信息:

using Elements;
using Elements.Geometry;

public class SpaceLayout
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public SpaceLayout(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void DefineSpaces()
    {
        // 首层空间(沿 Y 方向分为:南侧办公区 + 走廊 + 北侧办公区)
        DefineGroundFloorSpaces();

        // 二层空间(相同布局)
        DefineUpperFloorSpaces();

        // 楼梯间(贯穿两层)
        DefineStairwell();
    }

    private void DefineGroundFloorSpaces()
    {
        double z = 0;

        // 南侧办公区:Y = 0 ~ 6m,整宽 18m
        AddOfficeSpace("GF-南办公室", 0, 0, _p.LengthX, _p.GridSpacingY, z, _p.GroundFloorHeight);

        // 走廊:Y = 6m ~ 8m(2m 宽走廊)
        AddOfficeSpace("GF-走廊", 0, _p.GridSpacingY, _p.LengthX, 2.0, z, _p.GroundFloorHeight);

        // 北侧办公区:Y = 8m ~ 12m
        AddOfficeSpace("GF-北办公室", 0, _p.GridSpacingY + 2.0, _p.LengthX, _p.LengthY - _p.GridSpacingY - 2.0, z, _p.GroundFloorHeight);

        // 卫生间(东北角,2m × 3m)
        AddOfficeSpace("GF-卫生间", _p.LengthX - 3.0, _p.LengthY - 2.0, 3.0, 2.0, z, _p.GroundFloorHeight);
    }

    private void DefineUpperFloorSpaces()
    {
        double z = _p.GroundFloorHeight;

        AddOfficeSpace("UF-南办公室", 0, 0, _p.LengthX, _p.GridSpacingY, z, _p.UpperFloorHeight);
        AddOfficeSpace("UF-走廊", 0, _p.GridSpacingY, _p.LengthX, 2.0, z, _p.UpperFloorHeight);
        AddOfficeSpace("UF-北办公室", 0, _p.GridSpacingY + 2.0, _p.LengthX, _p.LengthY - _p.GridSpacingY - 2.0, z, _p.UpperFloorHeight);
        AddOfficeSpace("UF-卫生间", _p.LengthX - 3.0, _p.LengthY - 2.0, 3.0, 2.0, z, _p.UpperFloorHeight);
    }

    private void DefineStairwell()
    {
        double stairX = _p.LengthX / 2 - 0.75;
        double stairY = 2.0;

        var profile = new Profile(Polygon.Rectangle(1.5, 1.8));
        var stairwell = new Space(
            profile,
            0,                           // 从 ±0.000 开始
            _p.TotalHeight,              // 贯穿全部高度
            _p.GroundFloorHeight,        // 底面标高
            new Transform(stairX, stairY, 0)
        )
        {
            Name = "楼梯间",
            AdditionalProperties = new Dictionary<string, object>
            {
                { "Area", 1.5 * 1.8 },
                { "Category", "VerticalCirculation" }
            }
        };
        _model.AddElement(stairwell);
    }

    private void AddOfficeSpace(string name,
        double x, double y, double width, double depth,
        double z, double height)
    {
        var profile = new Profile(Polygon.Rectangle(width, depth));
        var space = new Space(
            profile,
            0,
            height,
            z,
            new Transform(x, y, 0)
        )
        {
            Name = name,
            AdditionalProperties = new Dictionary<string, object>
            {
                { "Area", width * depth },
                { "Category", name.Contains("走廊") ? "Corridor" : "Office" }
            }
        };
        _model.AddElement(space);

        // 输出面积信息
        Console.WriteLine($"  {name}: {width * depth:F1} m²");
    }
}

运行程序后,控制台输出各空间面积:

  GF-南办公室: 108.0 m²
  GF-走廊: 36.0 m²
  GF-北办公室: 72.0 m²
  GF-卫生间: 6.0 m²
  ...

16.8 机电管线示意

Elements 的 Elements.MEP 包提供了管路配件系统。本节用 MEP 中的 Fitting 概念创建给排水示意管线——立管与水平支管:

using Elements;
using Elements.Geometry;
using Elements.Geometry.Solids;

public class PlumbingLayout
{
    private readonly ModelBuilder _b;
    private readonly Model _model;
    private readonly BuildingParams _p;

    public PlumbingLayout(ModelBuilder builder)
    {
        _b = builder;
        _model = builder.Model;
        _p = builder.Params;
    }

    public void CreateRisers()
    {
        var copper = MaterialLibrary.Copper;
        var pipeRadius = 0.05;  // DN100 管径

        // 卫生间位置:东北角
        double toiletX = _p.LengthX - 1.5;
        double toiletY = _p.LengthY - 1.0;

        // 给水立管(从 ±0.000 到屋顶 +1m)
        CreatePipe(
            "给水立管",
            new Vector3(toiletX, toiletY, 0),
            new Vector3(toiletX, toiletY, _p.TotalHeight + 1.0),
            pipeRadius,
            copper
        );

        // 排水立管
        CreatePipe(
            "排水立管",
            new Vector3(toiletX + 0.3, toiletY, 0),
            new Vector3(toiletX + 0.3, toiletY, _p.TotalHeight + 1.0),
            pipeRadius * 1.2,
            copper
        );

        // 首层水平支管(从立管到卫生间洁具位置)
        CreatePipe(
            "给水支管-GF",
            new Vector3(toiletX, toiletY, 0.5),
            new Vector3(toiletX - 1.0, toiletY, 0.5),
            pipeRadius * 0.6,
            copper
        );

        // 二层水平支管
        CreatePipe(
            "给水支管-UF",
            new Vector3(toiletX, toiletY, _p.GroundFloorHeight + 0.5),
            new Vector3(toiletX - 1.0, toiletY, _p.GroundFloorHeight + 0.5),
            pipeRadius * 0.6,
            copper
        );
    }

    private void CreatePipe(string name, Vector3 start, Vector3 end,
        double radius, Material material)
    {
        var pipeLine = new Line(start, end);
        var pipeProfile = new Profile(
            new Circle(Vector3.Origin, radius).ToPolygon(24)
        );

        // 用 Beam 表示管道(圆柱截面沿线段拉伸)
        var pipe = new Beam(pipeLine, pipeProfile)
        {
            Name = name,
            Material = material
        };
        _model.AddElement(pipe);
    }
}

完整 MEP 设计(弯头、三通、变径等)需使用 Elements.MEP 包中的 FittingTreeFittingAdaptiveGrid。上述示意代码用圆形截面 Beam 近似表示管道,对可视化示意已足够。

16.9 序列化输出

完成模型构建后,将其导出为三种格式,满足不同场景的需求:

using Elements;
using Elements.Serialization.JSON;
using Elements.Serialization.glTF;
using Elements.Serialization.IFC;
using Elements.Serialization.SVG;

public class ExportPipeline
{
    private readonly Model _model;
    private readonly string _basePath;

    public ExportPipeline(Model model)
    {
        _model = model;
    }

    public void ExportAll(string basePath)
    {
        _basePath = basePath;

        // 确保输出目录存在
        Directory.CreateDirectory(basePath);

        ExportToJson();
        ExportToGlTF();
        ExportToIFC();
        ExportSectionToSVG();
    }

    private void ExportToJson()
    {
        string json = _model.ToJson();
        string path = Path.Combine(_basePath, "OfficeBuilding.json");
        File.WriteAllText(path, json);
        Console.WriteLine($"[导出] JSON → {path} ({json.Length} 字符)");
    }

    private void ExportToGlTF()
    {
        string path = Path.Combine(_basePath, "OfficeBuilding.glb");
        _model.ToGlTF(path);
        Console.WriteLine($"[导出] glTF/GLB → {path}");
    }

    private void ExportToIFC()
    {
        string path = Path.Combine(_basePath, "OfficeBuilding.ifc");
        _model.ToIFC(path);
        Console.WriteLine($"[导出] IFC → {path}");
    }

    private void ExportSectionToSVG()
    {
        // 在 Y = 6m 处(走廊中线)生成剖面
        var sectionPlane = new Plane(
            new Vector3(0, 6.0, 0),    // 平面上的点
            Vector3.YAxis               // 法线方向(Y 轴为法线 → 剖面垂直于 Y)
        );

        _model.Intersect(
            sectionPlane,
            out var polygons,
            out var beyond,
            out var lines
        );

        // 将剖面多边形导出为 SVG(需 Elements.Serialization.SVG 包)
        string path = Path.Combine(_basePath, "OfficeBuilding_Section.svg");
        var svgModel = new Model();
        // ... 将剖面结果添加到 svgModel 中 ...
        // svgModel.ToSVG(path);

        Console.WriteLine($"[导出] 剖面 → {path} (多边形: {polygons.Count}, 线段: {lines.Count})");
    }
}

16.9.1 导出格式对比

格式 用途 适用场景
JSON 存档与版本控制 Git diff、参数化变体管理、CI/CD 验证
glTF / GLB Web 3D 预览 浏览器在线查看、Three.js/Babylon.js 集成
IFC 2x3 / IFC 4 BIM 协作 交付给 Revit/ArchiCAD/Tekla 深化设计
SVG 图纸输出 施工图剖面、技术文档插图

16.10 代码结构总结与最佳实践

16.10.1 项目结构回顾

OfficeBuilding/
├── Program.cs                —— 入口,编排执行顺序
├── ModelBuilder.cs           —— 协调器,持有 Model 和参数
├── BuildingParams.cs         —— 全局参数(单一事实来源)
├── Structural/
│   ├── StructuralFrame.cs    —— 柱网、梁、楼板
│   └── StairsBuilder.cs      —— 梯段、平台、栏杆
├── Architectural/
│   ├── WallsBuilder.cs       —— 外墙、内隔墙
│   ├── CurtainWallBuilder.cs —— 幕墙面板
│   └── OpeningsBuilder.cs    —— 门洞、窗洞
├── MEP/
│   └── PlumbingLayout.cs     —— 给排水管线
├── Materials/
│   └── MaterialLibrary.cs    —— 材质单例
├── Spaces/
│   └── SpaceLayout.cs        —— 房间空间定义
└── Export/
    └── ExportPipeline.cs     —— 多格式导出

16.10.2 从本项目学到的关键模式

  1. 参数集中管理BuildingParams 类聚合了所有建筑参数(柱网间距、层高、构件尺寸等),避免了魔法数字分散在代码各处。修改设计只需改一个地方。

  2. 按专业领域拆分:结构、建筑、机电各自独立模块,互不干扰。每个模块通过构造函数接收 ModelBuilder 引用,共享 ModelParams

  3. 材质集中定义MaterialLibrary 确保所有构件使用一致的颜色和表面属性,修改材质时只需改一处。

  4. Grid2d 驱动定位:柱网使用 Grid2d 而非硬编码坐标,当柱距改变时所有柱、梁自动适应。

  5. IsVoid 开洞模式:门洞和窗洞通过在 Representation.SolidOperations 中添加 IsVoid = trueExtrude 实现,利用 CSG 布尔运算自动从墙体中减去。

  6. ElementDefinition + ElementInstance:重复出现的构件(如相同尺寸的柱)可以定义为一次,通过 CreateInstance 在不同位置实例化,减少数据冗余。

  7. 先结构后建筑再洞口:建模顺序很重要——先建结构框架,再加围护墙体和幕墙,最后放置开洞(因为开洞是对已有墙体的布尔减法操作)。

  8. Space 用于面积统计:用 Space 元素定义房间边界并自动计算面积,为面积指标、能耗分析提供数据基础。

  9. 导出与建模分离ExportPipeline 独立于建模逻辑,支持在不修改建模代码的情况下增加新的导出格式。

  10. 代码即文档Name 属性赋予每个元素有意义的名称(如 "柱-GF-3""外墙-南"),在 IFC 导出和 JSON 查看时一眼可知其用途。

16.10.3 扩展建议

本项目可作为以下方向的起点:

  • 参数化变体生成:将 BuildingParams 的不同取值作为输入,生成多个方案变体用于方案比选
  • 结构分析集成:将结构框架导出为 IFC,导入 SAP2000 / ETABS 进行结构计算
  • 能耗模拟:利用 Space 和围护结构信息,输出 gbXML 格式进行能耗分析
  • 4D 施工模拟:为每个元素添加时间属性(开始/结束日期),按施工进度分组导出
  • Web 审查平台:将 GLB 模型嵌入网页,配合 Three.js 实现剖切、测量、标注等审查功能

16.10.4 各章知识在本项目中的对应关系

章节 知识点 本项目中应用
第02章 Element / Model / 类型体系 所有元素的基类、Model 容器、元素生命周期
第03-04章 向量、曲线、多边形、Profile 柱网坐标、墙定位线、截面轮廓定义
第05章 Wall / Beam / Column / Floor 结构框架与建筑围护的核心构件
第08章 CSG 布尔运算 / IsVoid 通过 Opening 的 IsVoid 挤出在墙上开洞
第09章 Material / Color MaterialLibrary 统一管理所有材质
第10章 序列化:JSON / glTF / IFC / SVG ExportPipeline 多格式导出
第11-12章 Grid2d / 空间搜索 柱网定位、剖面查询
第13章 MEP 管线 给排水立管与水平支管示意
第14章 组件化生成 ElementDefinition + CreateInstance 减少冗余
第15章 Schema 驱动开发 BuildingParams 参数体系可 Schema 化

至此,Elements 全套教程已完结。从第一章的”Hello Wall”到本章的综合办公楼,你已掌握了用 C# 代码创建、管理、导出 BIM 模型的完整能力。Elements 的真正力量在于将建筑模型从静态文件转变为可计算的代码资产——自动化、版本化、可验证、可复用。希望本教程能成为你进入”编程式建筑设计”世界的可靠起点。

← 上一章 | 目录