基于 Revit API 的双跑楼梯参数化自动生成插件

本文整理我的本科毕业设计(南通大学·地理信息科学),技术方向是 BIM-GIS 融合里的一个具体切片:用 Revit API 自动生成合规的双跑楼梯。相比学位论文,这里砍掉了摘要、文献综述之类的”仪式性”内容,只留下我觉得真正值得写下来的技术决策和代码。

项目速览

  • 场景:Revit 二次开发,自动生成双跑平行楼梯
  • 技术栈:C# / .NET Framework 4.8 / Revit API 2022 / WPF(MVVM)/ ExternalEvent
  • 两个核心创新点:① 规范规则库 + 两阶段踏步解算;② 基于射线投射的净空前置校验,量取方向沿竖直轴,和 GB55031-2022 的要求一致;生成后还会导出室内拓扑 GeoJSON,接入 kepler.gl 做三维验证
  • 效果:单部楼梯生成流程约 642 ms,四类建筑功能类型的规范指标全部验证通过

为什么是楼梯

CIM(城市信息模型)想往室内延伸,绕不开一个问题:BIM 模型给了几何,但室内”人怎么从这一层走到那一层”这件事,还得有人把它显式地建出来。楼梯正是干这个活的构件——它不只是造型,更是连接楼层的拓扑节点,直接决定了室内导航、应急疏散这类应用能不能跑起来。

但楼梯建模在实际工作里挺”苦力”:在 Revit 里手动画一部双跑楼梯,还要顺手把国标里一堆踏步宽、踢面高、净宽、净空的要求过一遍,一不留神就漏检。于是我给自己挖了个坑:做一个插件,输入两个点和几个参数,自动生成一部符合规范的楼梯,而且合规检查要在建模之前做,不是等建完了再补救

现有做法的两个硬伤

在动手之前,先说清楚我想解决的是什么问题。

第一,规范校验基本靠人工。 踏步尺寸、梯段净宽、平台深度分别散落在《民用建筑通用规范》(GB55031-2022)、《住宅建筑规范》(GB55038-2025)、《建筑防火通用规范》(GB55037-2022)三本规范里,不同建筑类型对应的阈值还不一样。设计人员要逐一翻规范核对,效率低,也容易漏检。

第二,净空校验的检测方向本身是错的——这是我觉得最值得写的一点。规范要求净空要沿”踏步鼻端竖直向上”量取,但常规做法是等楼梯模型建完之后,用软件自带的碰撞检测去撞,而碰撞检测拉伸的方向是沿梯段坡面的法线方向,不是竖直方向。

拿典型住宅楼梯举例:踢面 175 mm、踏面 260 mm,梯段坡度角大概 33.9°。如果碰撞检测沿坡面法线量了 2200 mm 判定”无干涉”,这个 2200 mm 换算到竖直方向,实际对应的净高大概是 2651 mm,两者差了接近 20%。反过来说,一部竖直净空实际不够 2200 mm 的楼梯,完全可能在这种斜向检测下被判定为”合规”——这是系统性偏差,不是个别情况。而且碰撞检测必须等模型建好才能跑,一旦发现问题就要返工。

这两个问题分别对应我做的两块核心逻辑:规范规则库 + 两阶段踏步解算,和基于射线投射的净空前置校验

整体架构:MVVM + ExternalEvent

在讲这两块之前,得先说一个绕不过去的坑:Revit 的点拾取(PickPoint)和 WPF 的消息循环会打架。

如果用 ShowDialog 弹模态窗口,在窗口事件里直接调用 Selection.PickPoint 让用户在视图里点一个点,模态窗口的消息循环会跟 PickPoint 需要的消息循环互相冲突,轻则界面卡顿,重则出现孤立浮窗甚至死锁。

解决办法是用 Revit 提供的 ExternalEvent 机制:插件窗口改用非模态的 Show() 打开;点”拾取 P1/P2”时先 Hide() 窗口,等 PickPoint 返回后再 Show() 回来;点”生成”按钮时不直接操作 Revit 文档,而是把当前参数写进一个 Handler,再调用 ExternalEvent.Raise()——相当于给 Revit 主线程”留言”,Revit 会在下一个空闲帧回调 Handler.Execute(),在合法的 API 上下文里跑事务。

三层职责划分是标准 MVVM:

  • View:纯 XAML + 最少量 code-behind,只留 PickPoint 和关闭窗口这两件跟 Revit UI 强相关、没法搬到 ViewModel 的操作
  • ViewModel:所有校验逻辑、两阶段解算、”生成”按钮的可用状态判断
  • Handler(充当执行层):StairGlobalEventHandler.Execute(),真正调 Revit API 建楼梯的地方

架构图:MVVM + ExternalEvent 协作关系
图:View / ViewModel / ExternalEvent Handler 的职责划分

核心技术点一:规范驱动的规则库与两阶段踏步解算

规则库:一张表就够了

四类建筑功能类型,各自对应一组阈值,用字典存起来:

建筑类型 最小踏步宽 最大踢面高 最小梯段净宽 每跑最大级数 依据
住宅 260 mm 175 mm 1000 mm 18 级 GB55038-2025 §4.2.2
一般公共建筑 260 mm 165 mm 1100 mm 18 级 GB55031-2022 表5.3.9 第1行
附属楼梯(多层/高层) 260 mm 175 mm 1100 mm 18 级 GB55031-2022 表5.3.9 第2行
附属楼梯(超高层) 250 mm 180 mm 1100 mm 18 级 GB55031-2022 表5.3.9 第3行

净空统一要求:梯段净高 ≥ 2200 mm,休息平台净高 ≥ 2000 mm(GB55031-2022 §5.3.9)。

代码上就是一个 Dictionary<BuildingType, StairCodeParams>,用户在界面切建筑类型,ViewModel 立刻换一套阈值重新校验(下面是去掉大部分注释后的核心片段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static readonly Dictionary<BuildingType, StairCodeParams> Rules =
new Dictionary<BuildingType, StairCodeParams>
{
[BuildingType.Residential] = new StairCodeParams
{
MinTreadDepth = 260,
MaxRiserHeight = 175,
MinRunWidth = 1000,
MinLandingDepth = 1200,
MinClearHeight = 2200,
MaxStepsPerRun = 18,
RuleSource = "GB55038-2025 §4.2.2 / GB55031-2022 表5.3.9 第2行"
},
// Public / Attached / Supertall 同理……
};

新增建筑类型只要往字典里加一条,不用碰 ViewModel 或计算逻辑,扩展成本很低。

两阶段解算:先定级数,再定高度

这是我觉得比”直接指定踏步数”更贴近实际用法的一个设计。用户在 Revit 平面图里点两个点 P1、P2 确定楼梯的位置和朝向,这两点间的水平距离其实已经隐含了楼梯能摆下多少级踏步——没必要再让用户单独输入总级数。

设总踏步级数为 n,每级踏步宽为 b,休息平台深度为 L,P1P2 水平距离为 d。双跑楼梯两跑各占一半级数,两跑水平投影总长是 n×(b/2)(不是 n×b),加上平台深度就是 P1P2 距离:

1
d = n × (b / 2) + L

第一阶段,反过来解出总级数:

1
n = (d − L) × 2 / b

算出来的 n 如果是奇数,就调整成偶数——双跑楼梯要求两跑级数相等。

第二阶段,级数定了以后,用底部标高到顶部标高的实际竖向净高差 H 反推每级踢面高 h

1
h = H / (n + 2)

分母 +2 是因为 Revit 楼梯的踢面数比”用户感知的踏步级数”多算首尾各一个。

两阶段分开算的好处是:平面尺寸由 P1P2 的真实距离决定,竖向尺寸由真实层高决定,界面预览和最终生成的几何永远是同一套数字,不会出现”预览说 26 级,生成出来变 24 级”这种脱节。

对应代码(StairCalculator.Calculate 的核心分支):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static StairCalculationResult Calculate(
double totalHeightMm, int totalSteps, StairCodeParams rule)
{
if (totalSteps <= 0)
return new StairCalculationResult { IsValid = false };

double riserHeight = totalHeightMm / (totalSteps + 2);

// 奇数步时第一跑多一步(行业惯例)
int run1 = (totalSteps % 2 == 0)
? totalSteps / 2
: (totalSteps + 1) / 2;
int run2 = totalSteps - run1;

return new StairCalculationResult
{
TotalSteps = totalSteps,
RiserHeight = riserHeight,
Run1Steps = run1,
Run2Steps = run2,
IsValid = riserHeight <= rule.MaxRiserHeight
};
}

举个例子:层高 3600 mm、踏步宽 280 mm、平台深 1200 mm、P1P2 距离 4760 mm。代入第一阶段:n = (4760−1200)×2÷280 ≈ 25.4,调整为偶数后是 26 级,两跑各 13 级;代入第二阶段:h = 3600÷(26+2) ≈ 128.6 mm,小于住宅规范 175 mm 的上限,合规。

界面上这一整套校验是实时的:任何一个数值框改了,ViewModel 立刻重新解算、刷新预览区的合规徽标;输入不合法或超出规范范围时”生成”按钮直接变灰,从源头上不让不合规的参数进入 Revit 事务。

界面截图:参数输入与实时合规预览
图:几何参数区的实时合规徽标(绿色合规 / 橙色警告)

核心技术点二:基于射线投射的净空前置校验

思路

前面说了碰撞检测方向不对的问题。我的解法是:不等模型建完,在参数阶段直接算出每一级踏步面代表点的世界坐标,从这些点沿 +Z 方向发一条虚拟射线,用 ReferenceIntersector 找它撞到的第一个楼板/梁/屋顶,命中距离就是这一点的净空。射线只在内存里算,不产生任何 Revit 几何。

这里有个我觉得值得单独拎出来讲的重构点:楼梯生成阶段的坐标变换、和净空校验阶段的坐标变换,必须是同一套代码,否则两边一旦出现哪怕很小的实现差异(比如旋转轴写反、乘法顺序颠倒),净空检测点和实际梯段位置就会对不上,校验就失去意义。所以我把它抽成一个静态工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
internal static class CoordinateTransform
{
public static Transform CreateStairTransform(XYZ insertionPoint, double angleRad)
{
Transform rotate = Transform.CreateRotation(XYZ.BasisZ, angleRad);
Transform translate = Transform.CreateTranslation(insertionPoint);
// T × R:先旋转对齐方向,再平移到插入点
return translate.Multiply(rotate);
}

public static XYZ LocalToWorld(Transform tf, double localX, double localY, double elevFt)
{
// 只对 XY 做旋转+平移,Z 直接赋绝对高程,避免旋转矩阵污染高程值
XYZ worldXY = tf.OfPoint(new XYZ(localX, localY, 0));
return new XYZ(worldXY.X, worldXY.Y, elevFt);
}
}

生成楼梯的 StairGlobalEventHandler 和做净空校验的 ClearanceChecker 都调用这两个方法,杜绝了”两处各写一遍矩阵乘法”的重复代码风险——这也是后面 GeoJSON 拓扑坐标能做到零偏差的前提之一。

逐级发射射线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static double CastRayUp(ReferenceIntersector intersector, XYZ origin)
{
IList<ReferenceWithContext> hits = intersector.Find(origin, XYZ.BasisZ);
if (hits == null || hits.Count == 0)
return -1; // 上方无遮挡

double minProximity = double.MaxValue;
foreach (var ctx in hits)
{
// 过滤掉起点附近的自命中(比如恰好落在踏步面上)
if (ctx.Proximity > 1e-6 && ctx.Proximity < minProximity)
minProximity = ctx.Proximity;
}
return minProximity == double.MaxValue ? -1 : minProximity;
}

ClearanceChecker.Check() 会遍历第一跑、第二跑的每一级踏步面,再加上休息平台中心点,对每个点都调一次 CastRayUp,取全场最小净高跟规范阈值比较。射线只碰撞楼板(OST_Floors)、结构梁(OST_StructuralFraming)和屋顶(OST_Roofs),特意排除楼梯自身构件,避免射线打到自己身上误报。

检测结果送回 StairGlobalEventHandler:不合规时弹一个 Yes/No 对话框,显示具体差多少毫米、对应哪条规范,用户可以选择”仍然生成”或者”中止去调参数”——选中止的话不会对模型产生任何修改,因为这时候 StairsEditScope 还没打开。

示意图:射线净空校验原理
图:从踏步面代表点沿 +Z 方向发射虚拟射线,命中楼板/梁底面计算净高

截图:净空不合规时的确认弹窗
图:净空不足时的 Yes/No 确认对话框,显示实测净高与规范条文

生成之后:把楼梯变成一条”拓扑边”

楼梯造好只是第一步。既然目标是给 CIM 用,楼梯就不能只是个几何体,还得是室内路网图里的一条边——能跟走廊、房间连起来,供导航或疏散模拟使用。

所以插件在生成成功后会自动做一次全文档扫描,导出 GeoJSON:

  • 房间节点:每个 Room 的中心点,带楼层名和面积
  • 同层路径:Revit 自动算出的 PathOfTravel 折线
  • 楼梯跨层连接线:这是重点。每部楼梯输出 4 个航点——底层入口、第一跑顶端(平台入口)、第二跑底端(平台出口)、顶层出口,单跑楼梯退化成 2 点。两端的 Z 强制取标高高程(标高才是”真值”),中间两个航点用几何实际值。

拿到 GeoJSON 之后直接拖进 kepler.gl(Uber 开源的网页端地理可视化工具),按 type 字段上色,三层楼的路径会按 Z 轴自然分层,楼梯折线把相邻楼层连起来,室内竖向连通关系一眼就能看出来。

测试模型是一栋三层建筑(F1 ±0、F2 +3600、F3 +7200),验证了路径端点和楼梯航点在接驳处的平面坐标偏差——四个接合点全部是 0.0 mm,说明楼梯计算用的坐标基准和 Revit 自己算疏散路径时用的基准完全一致,这条室内路网在拓扑上是真正连通的,不是”看起来连上了”。

kepler.gl 三维拓扑可视化效果
图:房间节点(红)、行进路径(蓝)、楼梯跨层连接(黄)在 kepler.gl 中的三维分层展示

测试情况

功能上,四类建筑类型的规范阈值都跑通了,违规参数在界面阶段就被拦下来,不需要等生成完再去模型里核查。

性能上,对一次典型的双跑楼梯生成做了断点计时:

阶段 耗时 占比
规范校验 + 踏步解算 < 1 ms < 1%
净空预检(29 个采样点) 约 199 ms 31%
StairsEditScope 几何生成事务 约 378 ms 59%
室内拓扑提取 + GeoJSON 导出 约 7 ms 1%
其余辅助事务 约 57 ms 9%
全流程合计 约 642 ms 100%

大头是 Revit 内核提交楼梯几何本身的开销,跟插件代码关系不大;净空预检和拓扑导出这两个我自己加的模块合计占三成左右,不算瓶颈。相比人工在 Revit 里手画一部楼梯外加净空校验(通常要两三分钟),压到不到一秒,效率提升是数量级的。

生成效果:楼梯三维视图
图:Revit 中自动生成的双跑楼梯三维效果

局限,以及后面想做的

写完之后回头看,还有两块明显没打磨完:

  1. 楼梯类型太单一。 目前只支持双跑平行楼梯,剪刀梯、弧形梯、三跑折跑梯都没覆盖,通用性有限。
  2. 净空采样点还是太稀。 现在只在每级踏步的宽度中心取一个点发射射线,如果障碍物是单侧悬出的梁,可能会漏检——理论上加个宽度方向的多点采样就能补上,只是这次时间没排上。

再往后,比较想探索的方向是把 Revit 的项目基点(ProjectBasePoint)和测量点(SurveyPoint)接进来,算出真实地理坐标下的仿射变换,让这套室内拓扑能直接叠加到室外 GIS 底图上——这样才算真正打通”BIM 进 CIM”这条链路,而不是停在 Revit 内部自娱自乐。


写完回头看,这个项目最大的收获倒不是”用 Revit API 建了个楼梯”,而是把”规范条文”这种原本只存在于 PDF 文档里的东西,一步步拆成了可以在代码里跑起来的规则表、公式和射线检测——这个转化过程本身,可能比几何生成更值得花时间。如果你也在琢磨 BIM 和 GIS 怎么打通,欢迎交流。

本项目已开源(MIT 协议)

Inspired by industrial best practices. 欢迎在 GitHub 提交 Issue 或 PR 交流讨论!


基于 Revit API 的双跑楼梯参数化自动生成插件
https://809570.xyz/2026/07/01/C-Plugin-Revit/
作者
刘彪
发布于
2026年7月1日
许可协议