用AI开发Revit支吊架插件:我和AI来回改了五轮

用AI开发Revit支吊架插件:我和AI来回改了五轮
上一篇文章聊了AI写Revit插件能做到什么程度,这次来真的。
用一个完整项目做全程实录:装配式支吊架自动放置插件。从前期规则梳理、参数化族设计,到AI生成放置逻辑,再到调试踩坑——每一步都原原本本展示出来。
目的是让你看到:一个需求从脑子里的想法到最后可运行的代码,中间人和AI各自干了什么。
一、先把需求说清楚——这步比写代码重要
支吊架插件听起来简单,实际上是个规则密集型需求。在动手之前,我把需求用自然语言整理了一遍:
★
目标: 给排水、消防、暖通管道,在 MEP 模型里自动放置装配式支吊架。
规则:
读取管道外径和壁厚,查表确定支吊架规格(宽度、高度) 根据管道类型选择对应族(MEPHanger) 计算管道中心线,按间距要求(DN100以下1.5m,DN100以上2m)自动放置吊架 读取管道底部标高,自动设置支吊架底座标高 支吊架须连接(attach)到管道上,不能独立存在
写下来才发现,这5条规则里,第1条的”查表”、第3条的”计算间距”、第5条的”连接到管道”——每一条都是坑。
但在写提示词之前,我先把支吊架的规则整理成了一张”需求对照表”:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这个表非常重要——直接喂给AI,比让它自己推理规则要准确得多。
二、用AI设计参数化支吊架族
先说族的层面。很多人以为支吊架插件只写放置逻辑,其实族的设计同样关键——族参数不完整,放置代码写得再好也调不动。
第一轮:让AI生成族参数列表
提示词:
★
Revit 2020,MEP MEPHanger 类型族。
族参数要求:
宽度(吊架横担长度) 高度(吊架本体高度) 吊杆直径 夹具类型(法兰夹/卡箍/焊接) 族须支持嵌套:横担(HangerCrossArm)和吊杆(HangerRod)作为嵌套族,在主族里用”共享嵌套”方式载入。
不要生成完整族文件,只需要C#代码展示如何用 FamilyManager API 创建参数和嵌套结构。
AI返回:
publicvoidCreateHangerFamilyParameters(Family family)
{
var manager = family.FamilyManager;
// 添加族参数
var width = manager.AddParameter("吊架宽度",
BuiltInParameterGroup.PGIdentityData,
Autodesk.Revit.DB.SpecTypeId.Length, false);
var height = manager.AddParameter("吊架高度",
BuiltInParameterGroup.PGIdentityData,
Autodesk.Revit.DB.SpecTypeId.Length, false);
var rodDiameter = manager.AddParameter("吊杆直径",
BuiltInParameterGroup.PG_DATA,
Autodesk.Revit.DB.SpecTypeId.Length, false);
}
我的判断:SpecTypeId.Length 是 Revit 2021+ 的写法,2020 需要用 DisplayUnitType.DUT_MILLIMETERS。而且 FamilyManager.AddParameter 在族文档(FamilyDocument)里用法不同,这里直接用会报错。
修正:
// Revit 2020 写法
var width = manager.AddParameter("吊架宽度",
BuiltInParameterGroup.PGIdentityData,
ParameterType.Length, false);
参数类型用 ParameterType.Length,单位让Revit自动跟随项目单位系统。
第二轮:补充嵌套族结构
单独问嵌套族的问题:
提示词:
★
Revit 2020,.NET Framework 4.8。
在 MEP MEPHanger 主族里,载入两个嵌套共享族:
HangerCrossArm.rfa(横担,宽度参数可调) HangerRod.rfa(吊杆,直径参数可调) 给出用 FamilyManager 或 FamilyDocument API 载入嵌套族的 C# 代码片断,不需要完整addin。
AI返回:
var crossArmPath = @"C:\Families\HangerCrossArm.rfa";
var rodPath = @"C:\Families\HangerRod.rfa";
Family crossArmFamily = null;
Family rodFamily = null;
// 载入嵌套族
doc.LoadFamily(crossArmPath, out crossArmFamily);
doc.LoadFamily(rodPath, out rodFamily);
我的判断: 这段代码语法上没问题,但逻辑上有个根本性错误——**LoadFamily 是项目级操作,族文档(FamilyDocument)里没有这个方法**。在族编辑器里,应该用 Family.LoadFamilySymbol 或直接打开族文档。
修正起来比较麻烦,这部分我决定自己写,不用AI:
// 正确做法:先切换到项目文档准备族载入
// 放置时用 FamilySymbol 载入,这里族文档阶段只定义结构
// 嵌套族的载入在实际放置时完成
★
经验: 涉及族编辑器的 API,AI 几乎每次都会出错。族文档和项目文档是两套完全不同的 API 子集,AI 很容易混用。这部分宁可自己查文档,也不要花时间反复纠正AI。
三、用AI写支吊架放置逻辑
族的问题暂时告一段落,核心代码放在放置逻辑上。这部分AI帮得上忙,但同样需要逐轮审查。
第一轮:生成整体框架
提示词:
★
Revit 2020,MEP 管道 MEPHanger 自动放置插件,.NET Framework 4.8,C#。
需求:
用 FilteredElementCollector 收集当前视图中所有 MEP 管道(MEP管道类别:BuiltInCategory.OST_PipeCurves) 对每根管道,读取外径参数(外壁直径,单位mm)、管道中心线(LocationCurve) 按外径查表确定支吊架规格(DN≤50用200×150,DN50~100用250×200,DN>100用300×250) 按间距要求在管道上均布放置 MEPHanger(FamilySymbol 载入,Transaction 内放置) 吊架底座标高自动对齐管道底部标高 支吊架须通过 MEPFabricationUtils 连接到管道系统 只写数据提取和放置逻辑的核心代码,不需要UI。Transaction 已在外层处理。
AI返回(关键部分):
// 收集 MEP 管道
var pipes = new FilteredElementCollector(doc, doc.ActiveView.Id)
.OfCategory(BuiltInCategory.OST_PipeCurves)
.WhereElementIsNotElementType()
.ToElements()
.Cast<Pipe>()
.ToList();
// 查表确定吊架规格
(string width, string height) GetSpec(double diameterMm)
{
if (diameterMm <= 50) return ("200", "150");
if (diameterMm <= 100) return ("250", "200");
return ("300", "250");
}
// 放置支吊架
using (var tx = new Transaction(doc, "放置支吊架"))
{
tx.Start();
foreach (var pipe in pipes)
{
// 获取管道外径
double diameter = pipe.get_Parameter(
BuiltInParameter.RBS_PIPE_OUTER_DIAMETER)?
.AsDouble() ?? 0;
double diameterMm = UnitUtils.ConvertFromInternalUnits(
diameter, UnitTypeId.Millimeters); // ❌ 又是2021+写法
// 获取管道中心线和长度
var location = pipe.Location as LocationCurve;
if (location == null) continue;
Curve centerLine = location.Curve;
double pipeLength = centerLine.Length;
// 计算放置点
var spec = GetSpec(diameterMm);
double spacing = diameterMm <= 100 ? 1.5 : 2.0; // 米
for (double dist = 0; dist < pipeLength; dist += spacing)
{
XYZ point = centerLine.Evaluate(dist, true);
// 获取管道底部标高(用于设置吊架底座)
double pipeBottom = pipe.ReferenceLevel.Elevation
+ pipe.get_Parameter(BuiltInParameter.RBS_PIPE_BOTTOM_ELEVATION)?.AsDouble()
?? 0;
// 载入并放置 MEPHanger
FamilySymbol hangerSymbol = GetOrLoadHangerSymbol(
doc, spec.width, spec.height);
MEPHanger hanger = MEPHanger.Create(doc,
hangerSymbol.Id, pipe.MEPSystem.Id, point);
// 设置底座标高
hanger.get_Parameter(
BuiltInParameter.RBS_HANGER_BOTTOM_ELEVATION)?
.Set(pipeBottom);
}
}
tx.Commit();
}
我的审查:
|
|
|
|
|---|---|---|
UnitTypeId.Millimeters
|
|
|
pipe.MEPSystem.Id
|
|
|
MEPHanger.Create
|
|
|
RBS_HANGER_BOTTOM_ELEVATION
|
|
|
|
|
|
|
问题不少,但核心思路是对的——管道收集、间距计算、标高读取,这些逻辑AI理解得不错。
第二轮:针对最难的 MEPHanger 放置单独提问
MEPHanger.Create 的 API 签名是最大难点,AI的输出里这个调用完全不对。
我换了个提示词专门问这个:
提示词:
★
Revit 2020 API,MEPHanger.Create 方法的正确签名是什么?给一个在管道上放置 MEPHanger 的完整代码示例,包含 FamilySymbol 的获取和 MEPHanger 实例的创建。不要猜测,给 Revit API 文档中真实存在的方法签名。
AI这次给出的代码明显保守了很多——它开始用”我不确定具体参数,建议查文档”的方式回答了。这其实是个好信号,说明它识别到了这个接口的复杂性。
最终我自己在 Revit API 文档里查到了正确写法:
// 正确的 MEPHanger.Create 签名(Revit 2020)
MEPHanger.Create(Document document, FamilySymbol familySymbol,
ElementId MEPSystemId, XYZ point);
// 但实际项目中,非系统管道没有有效 MEPSystemId
// 解决方案:传 ElementId.InvalidElementId,并在放置后单独处理连接
// 或者直接用 FamilyInstance.Create,而非 MEPHanger.Create
// 这里采用后者,绕过 MEP 系统依赖
FamilyInstance hanger = doc.Create.NewFamilyInstance(
point, hangerSymbol,
pipe, Autodesk.Revit.DB.Structure.StructuralType.NonStructural);
★
经验: MEPHanger 是 Revit MEP 模块的专用类型,创建它需要管道在有效的 MEP 系统内。如果是非系统管道(没有连接到任何系统),直接用
FamilyInstance.Create更简单,不影响实际功能。
第三轮:解决标高和连接问题
标高参数的设置是另一个坑。
AI 猜的 RBS_HANGER_BOTTOM_ELEVATION 根本不存在。实际上 MEPHanger 的标高通过 Structure.Utilities.MechanicalUtils 或者直接用 Instance.SetParameter 设置。
修正后的代码:
// 设置吊架底座标高(对齐管道底部)
// MEPHanger 的底座标高 = 管道中心标高 - 管道半径
double pipeRadius = diameter / 2;
double pipeCenterElevation = pipe.ReferenceLevel.Elevation
+ pipe.get_Parameter(BuiltInParameter.RBS_PIPE_TOP_ELEVATION)?
.AsDouble() ?? 0;
double hangerBottomElevation = pipeCenterElevation - pipeRadius;
// 直接用 ElementId 设置标高
hanger.get_Parameter(BuiltInParameter.INSTANCE_ELEVATION_PARAM)?
.Set(hangerBottomElevation);
四、最终代码:可用版本
经过三轮对话和自行修正,最终可运行的核心逻辑如下:
public Result ExecuteCore(UIDocument uiDoc, Document doc, ExternalCommandData cmdData)
{
// 1. 收集 MEP 管道
var pipes = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_PipeCurves)
.WhereElementIsNotElementType()
.ToElements()
.Cast<Pipe>()
.Where(p => p.get_Parameter(BuiltInParameter.RBS_PIPE_OUTER_DIAMETER) != null)
.ToList();
if (pipes.Count == 0)
{
TaskDialog.Show("提示", "当前模型中未找到 MEP 管道。");
return Result.Succeeded;
}
// 2. 获取或载入支吊架族
var hangerSymbol = GetOrLoadHangerSymbol(doc);
if (hangerSymbol == null)
{
TaskDialog.Show("错误", "未找到支吊架族 M_Hanger,请先载入族文件。");
return Result.Failed;
}
// 3. Transaction 内批量放置
using (var tx = new Transaction(doc, "批量放置支吊架"))
{
tx.Start();
int placedCount = 0;
foreach (var pipe in pipes)
{
// 读取管道外径(内部单位:英尺,转mm)
double diameterFt = pipe.get_Parameter(
BuiltInParameter.RBS_PIPE_OUTER_DIAMETER)?.AsDouble() ?? 0;
double diameterMm = UnitUtils.ConvertFromInternalUnits(
diameterFt, DisplayUnitType.DUT_MILLIMETERS);
// 查表确定间距
double spacingM = diameterMm <= 100 ? 1.5 : 2.0;
// 获取管道中心线
var location = pipe.Location as LocationCurve;
if (location == null) continue;
Curve centerLine = location.Curve;
double pipeLengthFt = centerLine.Length;
// 转米计算放置点数
double pipeLengthM = UnitUtils.ConvertFromInternalUnits(
pipeLengthFt, DisplayUnitType.DUT_METERS);
int count = Math.Max(1, (int)(pipeLengthM / spacingM));
double stepFt = pipeLengthFt / count;
// 计算管道底部标高(内部单位)
double pipeBottomElev = pipe.ReferenceLevel.Elevation
- diameterFt / 2; // 管道中心 - 半径 = 管道底部
for (int i = 0; i <= count; i++)
{
double dist = i * stepFt;
XYZ point = centerLine.Evaluate(dist, true);
// 用 FamilyInstance.Create 放置(绕过 MEP 系统限制)
FamilyInstance hanger = doc.Create.NewFamilyInstance(
point, hangerSymbol,
pipe, Autodesk.Revit.DB.Structure.StructuralType.NonStructural);
// 设置标高(内部单位)
hanger.get_Parameter(BuiltInParameter.INSTANCE_ELEVATION_PARAM)?
.Set(pipeBottomElev);
placedCount++;
}
}
tx.Commit();
TaskDialog.Show("完成", $"共放置支吊架 {placedCount} 个。");
return Result.Succeeded;
}
}
// 辅助:获取或载入支吊架族符号
private FamilySymbol GetOrLoadHangerSymbol(Document doc)
{
// 先尝试从当前模型中查找
var collector = new FilteredElementCollector(doc)
.OfCategory(BuiltInCategory.OST_PipeAccessories)
.WhereElementIsElementType()
.FirstOrDefault(e => e.Name.Contains("Hanger")) as FamilySymbol;
if (collector != null) return collector;
// 未找到则尝试载入族文件
string familyPath = @"C:\Families\M_Hanger.rfa";
Family family;
if (!doc.LoadFamily(familyPath, out family))
returnnull;
return family.GetFamilySymbolIds()
.Select(id => doc.GetElement(id) as FamilySymbol)
.FirstOrDefault();
}
五、我从这五轮中学到了什么
AI 特别擅长的部分
-
管道数据提取: FilteredElementCollector+get_Parameter的写法,AI很熟练 -
间距计算、循环逻辑:纯数学/逻辑,零出错 -
报错解释:把编译错误或运行时异常粘给AI,它几乎每次都能给出有价值的分析
AI 频繁出错的部分
|
|
|
|
|---|---|---|
|
|
UnitTypeId.*
DisplayUnitType.DUT_* |
|
|
|
MEPHanger.Create
|
|
|
|
RBS_HANGER_BOTTOM_ELEVATION |
|
|
|
doc.LoadFamily
|
|
最有效的提问方式
[场景]:Revit 2020,MEP 管道开发,.NET Framework 4.8,C#
[需求]:(一句话描述要做什么)
[约束]:只写(具体功能),不包含其他;处理(边界情况)
[已知的]:管道外径用 RBS_PIPE_OUTER_DIAMETER 获取
把你已知的上下文也告诉AI,能大幅减少它瞎猜的概率。
六、一个可复用的提示词模板
经过这整个项目,总结出一个专门针对 Revit MEP 开发的提示词模板:
[场景]:Revit 2020,MEP 插件开发,.NET Framework 4.8,C#
[需求]:(具体功能描述)
[上下文]:
- 管道外径参数:BuiltInParameter.RBS_PIPE_OUTER_DIAMETER
- 管道中心线:LocationCurve + Curve.Length / Evaluate
- 单位:内部单位是英尺,换算用 DisplayUnitType.DUT_MILLIMETERS / DUT_METERS
[约束]:
- 只写(X功能),不包含UI和Transaction封装
- 不写 MEPHanger.Create,用 FamilyInstance.Create 代替
[参考]:(可选,贴现有相关代码)
模板里最重要的两件事:
-
版本说清楚(Revit 2020),单位写法随之确定 -
把你查好的参数名和接口名告诉它,别让它自己猜
写在最后
这次实录最有价值的发现不是哪个API怎么写,而是人和AI的分工边界到底在哪:
-
AI负责重复性逻辑、通用C#、报错分析 -
你负责业务规则、版本判断、复杂API查证
把规则整理成表格、把上下文提前告知——这两件事做完,你会发现AI的输出质量一下子从60分跳到85分。剩下那15%,是调试环节的工作,而调试本身就是你作为Revit开发者必须具备的能力。
工具是加速器,不是替代者。
优易科技 专注BIM软件研发与数字化建造解决方案
优易BIM助手,懂技术,更懂落地。
欢迎转发
夜雨聆风