Revit 二次开发属于计算几何开发,调试时面临一个根本痛点:调试器里只能看到 XYZ(12847.3, -5621.8, 3200.0) 这类抽象数字,只有程序跑完整个方法才能在 Revit 视口里看到真实图形

这不是工具不够的问题,是**“调试器冻结进程”与”Revit 单线程渲染”之间的架构性矛盾**。

这篇是我把 Revit / CAD / Rhino / Houdini / Unreal / Polyscope 等 30+ 生态横向走了一遍后整理的笔记。提炼出 5 种设计哲学,给出工业级落地架构。

一、问题本质

Revit API 严格单线程,所有 Document / 视口操作必须在主线程。任何试图在冻结 Revit 里绘图的方案都注定失败——

  • 单步调试时 VS 冻结主线程
  • 即使用 Document.Transparent(curve) 这种临时图元 API,几何也只是被排队,要等调试器解冻 + 渲染循环恢复才能看见
  • Immediate Window 的 function evaluation 会短暂解冻执行那一行,但解冻时间只够运行一行代码,Revit 根本没机会跑渲染循环

几何位置 / 方向 / 法向量是否正确,仅靠数字推算难度太高。

二、Revit 生态内的头部方案

Tier 1 — RevitLookup + Geometry Visualization ⭐

  • 组织lookup-foundation(原作者 Jim Awe @ Autodesk,Jeremy Tammik 开源,现由 Nice3point / Roman Karpovich 主维护)
  • 关键事件:2024 年 Nice3point 通过 PR #245 加入 Geometry Visualization 功能,底层用 DirectContext3D
  • 能力
    • Solid Visualization — 整个实体的内外结构高亮
    • Mesh Visualization — 顶点 / 网格拓扑
    • Curve Visualization — 线 / 弧 / 样条的曲率与连续性
  • 用法:右键任意对象 → 直接在视口渲染出来,不污染模型
  • 结论:做 Revit 二次开发第一步就要装

Tier 2 — Autodesk 官方 API

API时代场景特点
DirectContext3D2017+首选直接喂 GPU 顶点/索引缓冲,不创建元素,能渲染百万级点/线/面,支持 Live Jig
Analysis Visualization Framework (AVF)带色分析SpatialFieldManager,适合温度/信号/日照着色。坑:只存内存,DesignOption 里崩溃
TemporaryGraphicsManager2022+交互式视图上显示带点击事件的图片/图标
BRepBuilder复杂 B-rep构造自定义 B-rep 后丢给 DirectShape / DirectContext3D 显示

Tier 3 — 经典方案

  • DirectShape + DirectShapeFramework (NuGet) — 物化成真实 Revit 元素。优点:可选中测量出图;缺点:污染模型需事务
  • ModelLine / ReferencePoint 打点法 — Jeremy Tammik 2012 年的土办法,3 条短 ModelLine 做十字光标
  • 外部导出:DXF / OBJ / STL / JSON + Three.js viewer

Tier 4 — 开发流程配套

工具作用
Add-in Manager(Autodesk SDK)Read Only Mode 下不重启 Revit 热加载 DLL
RevitAddInManager(chuongmep 开源)社区加速版
ricaun.RevitAddin.VisualStudioDebug自动化 Attach to Process
ricaun.RevitTestNUnit 测试直接跑在 Revit 进程内

三、断点冻结问题的根本解法

为什么 Document.Transparent(curve) 在断点处画不出来

Nice3point 的 TempGraphics 扩展底层走 Revit 2022+ 的 TemporaryGraphicsManager,它只是把几何塞进 Revit 的图形队列,而实际 GPU 绘制必须等:

  1. Revit 主线程回到消息循环
  2. 渲染管线下一帧被调度

断点命中时这两步都被 VS 冻结住了

唯一出路 — 把渲染搬出 Revit 进程。

架构 A — 外部 Viewer + IPC ⭐ 最推荐

┌────────────────────────────┐      Named Pipe       ┌────────────────────────────┐
│  Revit.exe (被 VS 冻结)    │  "revit-debug-viz"    │  RevitDebugViewer.exe      │
│                            │  ──────────────────▶  │  (WPF + HelixToolkit)      │
│  YourAddin.dll             │  JSON / MessagePack   │                            │
│    ├─ DebugViz.Push(xyz)  ─┤                       │  3D 视口                    │
│    ├─ DebugViz.Push(curve)─┤  同步写,不依赖渲染   │  ├─ 点/线/面列表           │
│    ├─ DebugViz.Push(solid)─┤                       │  ├─ 颜色 / 标签            │
│    └─ DebugViz.Clear()     │                       │  └─ 清屏 / 保存 / 重放     │
└────────────────────────────┘                       └────────────────────────────┘

为什么能在断点处工作

  • 命名管道 Write 是内核级同步 IO,不依赖 Revit 的消息泵或渲染循环
  • VS 的 function evaluation 临时解冻主线程足以完成一次管道写入
  • 外部 viewer 进程没有被调试,消息循环一直在跑,收到数据立即画

调试体验

(断点命中在第 47 行)
F10 →  Immediate Window:  DebugViz.Push(intersection, Color.Red)
      →  外部 viewer 窗口立即出现红色交点 ✨
F10 →  DebugViz.Push(projectedCurve, Color.Yellow)
      →  viewer 里出现黄色曲线 ✨
F10 →  DebugViz.Clear()

架构 B — 自定义 Visual Studio Debugger Visualizer

Microsoft 官方机制,和 OpenCV 的 Image Watch(解决图像调试的同类问题)同源:

  • 继承 DialogDebuggerVisualizer,UI 用 Helix Toolkit (WPF 3D)
  • VisualizerObjectSource,在调试端把 XYZ / Curve / Solid 提取成可序列化 DTO
  • 打包 DLL 放到 Documents\Visual Studio 2022\Visualizers\
  • UI 运行在 VS 进程里,不被 Revit 冻结影响

优点:原生集成 缺点:Revit 类型不是 [Serializable];修改 Visualizer DLL 要重启 VS;只看”当前变量”没法叠加

架构 C — 现成 VS 插件(半适配)

工具覆盖情况结论
Graphical Debugging (Adam Wulkiewicz)C# ✅ / 自定义类型 ✅ / 3D ❌只支持 2D,对 Revit 不够
ILNumerics Array Visualizer3D ✅ / 只吃 double[,]要先转矩阵,能用不顺手
OpenCV Image Watch只看图像活教材不可直接用

结论没有一个现成插件能直接吃 Revit 几何类型,只能自己写架构 A 或 B。

🔥 杀手级技巧 — Tracepoint(断点不停)

VS 断点右键 → Actions → 填入 {DebugViz.Push(curve, "step-" + i)} 并勾 “Continue execution”:

  • 断点命中不停,只执行表达式继续跑
  • 在算法 5 个关键位置放 5 个 Tracepoint → 一口气跑完 → 外部 viewer 里出现带标签的 5 个中间几何
  • 整个算法的”运行轨迹”一次成图

几何算法很多 bug 是趋势型的(loop 角度累积偏移等),轨迹图一眼看出规律。

四、横向对比:其他三维生态怎么做

4.1 CAD 二次开发

AutoCAD — Transient Graphics + Jig

AutoCAD 2009 起 TransientManager.AddTransient(entity, ...) 不入数据库、不污染 dwg、无需事务。Jig 更早,命令式接管鼠标事件 + 实时重绘(按住鼠标画圆预览)。仍然没解决断点冻结问题

SolidWorks — IBody2.Display3

Dim tempBody As Body2 = modeler.CreateBodyFromBox3(...)
tempBody.Display3(modelDoc, rgbColor, swTempBodySelectOptions_e.swTempBodySelectable)

临时 body 可选中旋转,不进 FeatureTree。和 Revit 的 DirectShape 同思路。

4.2 NURBS / 建模软件

Rhino / Grasshopper — DisplayConduit ⭐

RhinoCommon 提供 DisplayConduit 基类,优雅的 Debug Draw 典范

class DebugConduit : DisplayConduit
{
    public List<Line> Lines = new();
    public List<Point3d> Points = new();
 
    protected override void DrawForeground(DrawEventArgs e)
    {
        foreach (var l in Lines) e.Display.DrawLine(l, Color.Red, 2);
        foreach (var p in Points) e.Display.DrawPoint(p, Color.Yellow);
    }
}
 
// 用的时候:
var conduit = new DebugConduit { Enabled = true };
conduit.Lines.Add(someLine);
RhinoDoc.ActiveDoc.Views.Redraw();
  • 不进文档、不需事务、一行代码加几何
  • Enabled = false 立即关掉
  • 所有 CAD/CAM 里最接近游戏引擎哲学的方案
  • 仍然被断点冻结问题困扰

4.3 DCC 软件

Houdini — Guide Geometry + Intermediate Output ⭐⭐

Houdini 的解决方案极其独特

  • 每个 SOP 节点的输出都可以直接点击查看 —— 架构上就是”每一步都天然持久化可查看”
  • 写自定义工具时定义 Python Viewer State + 注册 Guide Geometry
  • HDA 里可以加专门的 “visualize” 输出口,算法跑完所有中间结果可切换浏览

本质洞察:Houdini 压根不需要”断点时看中间结果”,因为节点式设计让每一步都是一个可持久查看的节点输出。这是架构层面的降维解决方案。

Blender — gpu Module + Draw Handler

import gpu
from gpu_extras.batch import batch_for_shader
 
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES', {"pos": [(0,0,0), (1,1,1)]})
 
def draw():
    shader.bind()
    shader.uniform_float("color", (1, 0, 0, 1))
    batch.draw(shader)
 
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

每帧往视口自定义绘制,走 GPU。与 Rhino DisplayConduit 同哲学。

4.4 游戏引擎(天花板)

Unreal Engine — DrawDebugHelpers 黄金标准 ⭐⭐⭐

DrawDebugLine(GetWorld(), Start, End, FColor::Red, false, 5.0f, 0, 2.0f);
DrawDebugSphere(GetWorld(), Center, 50.0f, 12, FColor::Green, false, 5.0f);
DrawDebugBox(...);  DrawDebugCone(...);  DrawDebugString(...);

革命性特性:

  • 一行代码就画,不需要事务/场景图/文档
  • 每条线都能设持续时间(5 秒后自动消失)
  • Persistent 模式下暂停游戏也能看
  • Tick / 构造函数 / 任意事件回调里都能调用

Unity — Gizmos + Debug.DrawLine

  • Debug.DrawLine(from, to, color, duration) — 运行时任意位置
  • Gizmos.DrawLine / Gizmos.DrawSphereOnDrawGizmos() 回调里画,Scene 视图常驻

游戏引擎为什么做得这么好? 它们天生是帧循环架构,每一帧都在重新绘制整个场景,“加一条调试线”只是往这一帧的绘制列表里追加一项,下一帧自然出现。没有事务概念,没有文档状态概念,也没有 UI 主线程独占限制。

4.5 学术 / 科研几何库

Polyscope — 科研领域的”标准答案” ⭐⭐⭐

普林斯顿 Nick Sharp + Keenan Crane 维护的 C++/Python 几何查看器:

polyscope::init();
polyscope::registerSurfaceMesh("my mesh", V, F);
polyscope::registerPointCloud("sampled points", points);
polyscope::getSurfaceMesh("my mesh")->addVertexScalarQuantity("curvature", K);
polyscope::show();
  • < 10 行代码启动完整 3D viewer
  • structures + quantities 的抽象 —— 注册结构,往上挂任意数量数据(标量/向量/颜色)
  • 不霸占主循环,随时可嵌入任何 C++ 项目
  • 科研人员调试 Laplacian / 测地线 / 曲率算法的标配

libigl Viewer

ETH + NYU 的学术库,内置 debug API:

igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.data().add_points(P, Eigen::RowVector3d(1,0,0));
viewer.data().add_edges(P1, P2, Eigen::RowVector3d(0,1,0));
viewer.data().add_label(pos, "step 3");
viewer.launch();

OpenCASCADE — Inspector ⭐

OCCT(世界最大开源 B-rep 几何内核,FreeCAD 基础)的专用调试工具:

  • Qt 写的独立应用
  • 插件化架构:DFBrowser(看 OCAF 文档)/ VInspector(看 AIS 上下文)/ ShapeView(看 TopoDS_Shape 拓扑树)
  • 交互式浏览任何 OCCT 数据结构,包括 Face / Edge / Vertex 的拓扑
  • “专用调试检查器”路线的代表作,和 RevitLookup 完全对应但以桌面应用形态存在

五、5 种设计哲学

走完一圈之后,所有方案可以归到 5 种哲学:

#哲学代表核心做法
AImmediate Mode Debug DrawUnreal / Unity / Blender gpu每帧重画,一行代码即可,不走文档/事务
BDisplay Conduit / OverlayRhino DisplayConduit / Revit DirectContext3D继承基类,在 Draw 回调画,可开关
CTransient EntityAutoCAD Transient / SW Display3 / Revit DirectShape宿主 API 提供”临时几何”,不入文档树
DIntermediate Node OutputHoudini HDA visualize / libigl GPP中间结果作为算法的一等公民输出项,事后浏览
EExternal Inspector / Out-of-process ViewerOCCT Inspector / Polyscope / RevitLookup独立进程,IPC / 数据注册接收,UI 不受宿主影响

六、对 Revit 开发者的 5 个洞察

洞察 1 — 断点冻结问题只有”外部进程”能根治

游戏引擎和 Rhino 的 Debug Draw 再优雅,也解决不了断点冻结。它们体验好是因为开发者**不打断点,改用”运行时 Debug Draw + 暂停观察”**的流程。

思维转变:不要指望在断点处看到几何,而是让几何在运行时被持续记录,断点只用来看数据。Revit 里等价 = DirectContext3D + 保留所有历史帧 + 断点时从列表回溯。

洞察 2 — Houdini 的”节点式中间结果”是终极方案

Houdini 根本不需要”调试可视化”这个词 —— 节点网络让每一步天然持久化可查看。

对 Revit:把几何算法拆解成显式的阶段,每阶段输出 DebugViz.Push(namedStage: "step-3-projection")。外部 viewer 里滑动时间轴回看每一步

洞察 3 — Polyscope 的”Structure + Quantity”抽象值得照抄

科研库的 API 设计比 CAD 更成熟:不是”画一条线”,而是”注册一个结构,往上挂数据”:

DebugViz.Register("wall-01", wallSolid);
DebugViz.Register("wall-01").AddScalar("thickness-deviation", devArray);
DebugViz.Register("wall-01").AddVector("face-normals", normals);

不仅能看几何,还能把每个面的偏差值、法向量一起叠在同一个结构上着色显示。

洞察 4 — Unreal 的 duration 参数是神来之笔

DrawDebugLine(..., duration=5.0f) —— 这条线 5 秒后自动消失。对应 Revit 外部 viewer:

每个 Push 支持 fadeAfter: TimeSpan,算法跑完调试几何自动淡出。配合”按步骤 Push”,能看到几何按时间顺序”流动”地生成,像一场动画而非静态图。

洞察 5 — OCCT Inspector 证明”外挂调试器”路线可行

OCCT Inspector 是独立 Qt 应用,通过挂钩 OCCT AIS Context / OCAF Document 做深度检查。这和”外部 WPF viewer + 命名管道”同一路线,在一个和 Revit 一样复杂的 B-rep 系统完全工业化落地了

七、终极调试体验(目标架构)

┌─────────────────────────────────────────────────────┐
│          Revit 插件代码                              │
│                                                     │
│  DebugViz.Push(wallSolid,                           │
│      name: "step-3-wall",                           │
│      color: Red,                                    │
│      duration: 10s)                                 │
│                                                     │
│  DebugViz.Push(wallSolid)                           │
│      .AddVectorField("normals", normals)            │
│      .AddScalar("deviation", deviations);           │
└─────────────┬───────────────────────────────────────┘
              │ 命名管道 (同步写)
              ▼
┌─────────────────────────────────────────────────────┐
│     RevitDebugViewer.exe (独立进程)                 │
│                                                     │
│   🎨 HelixToolkit 3D 视口                            │
│   📜 时间轴 (可回放算法每一步)                       │
│   🗂 结构树 (Polyscope 风格 structures + quantities) │
│   🎛 ImGui 风格控制面板 (显隐/颜色/透明度)           │
│   💾 Save Scene → 问题场景可序列化复现               │
│   🔄 Load Scene → 发给同事 / 发给 AI 分析            │
└─────────────────────────────────────────────────────┘

融合的设计来源

  • Unreal 的 duration 自动过期
  • Polyscope 的 structure + quantity 抽象
  • Houdini 的时间轴 / 阶段回放
  • Rhino DisplayConduit 的一行代码即可用
  • OCCT Inspector 的 out-of-process 架构
  • RevitLookup 的项目内对接风格

八、可复用的”巨人肩膀”

组件参考实现能复用的部分
外部 viewer 的 UI 哲学Polyscopestructures + quantities API 设计
外部 viewer 的 3D 渲染HelixToolkit.WpfHelixViewport3D、相机、拾取
算法阶段可视化libigl GPP ProcessVisualizer按阶段切换查看的 UI 思路
宿主端 Debug Draw APIRhino DisplayConduitDebugConduit.Lines.Add(...) 调用风格
持续时间 / 淡出Unreal DrawDebugHelpersduration 参数设计
进程间调试检查器OCCT InspectorQt 架构、插件化、场景保存导出

主要参考资料


调试器冻结进程 + 单线程渲染的矛盾,在 Revit、AutoCAD、Rhino 里都存在;游戏引擎和科研库走另一条路 —— 不打断点,让几何在帧循环里自然出现。把渲染搬出宿主进程,是工业级 CAD 二次开发能借鉴的最稳妥路线。