一个由单人维护、近零外部依赖、覆盖从底层 SIMD 到 AI 协议栈的 .NET 8 几何 SDK。本文从架构、设计哲学、代码生成系统、命令引擎到 AI 集成,逐层拆解它的精妙之处。
一、这个项目是什么?
Ara3D SDK 是一个面向建筑、工程与施工(AEC)领域的 C# 3D 几何引擎,发布在 NuGet 上。它不是什么庞然大物,而是一个 单人维护 的项目,却覆盖了从底层集合库、几何算法、文件格式、BIM 数据模型,到 Roslyn 动态编译引擎和 MCP(Model Context Protocol)AI 协议接口的完整链条。
来看看它的规模:src/ 下 30+ 个项目 ,Plato 代码生成系统产出 100+ 类型 ,Bowerbird 命令系统支撑 Studio 和 Revit 双端脚本 ,vendor 了 Revit 2015-2025 全系列 API + 3ds Max + Maya + Navisworks + FBX 。而所有这些依赖的外部 NuGet 包,核心库(Collections、Geometry、Utils)一个都没有。
这是一份值得每一个 .NET 开发者、几何算法爱好者、以及"单人项目如何做到长生命周期可持续"的思考者细读的技术样本。
二、核心设计哲学:FP + 不可变 + 接口扩展
翻阅项目的 AGENTS.md,两条原则凌驾于一切之上:
- 代码必须能工作。 正确性优先。一个能工作的简单版本胜过坏掉的复杂版本。
- 代码必须易于改进和重构。 所有其他原则服务于这个目标。
这是典型的 现实主义工程哲学。作者没有追求"企业级抽象"和"最佳实践"的教条,而是选择了一条更务实、也更具个人风格的路线。具体的代码准则包括:
不可变扩展函数 + 最小接口
这是整个 SDK 中最高频出现的模式。看看这个:
csharp
// 小接口,行为通过扩展函数添加
public static bool InRange<T>(this IReadOnlyList<T> self, int n)
=> n >= 0 && n < self.Count;
// 几何操作也是如此
public static TriangleMesh3D Triangulate(this QuadMesh3D mesh)
=> new(mesh.Points, mesh.FaceIndices.QuadFacesToTriFaces());
接口只定义"是什么",扩展函数定义"能做什么"。这就意味着你可以自由地添加新行为而不破坏任何调用方,同时保持了纯函数的不可变性------每个操作返回新值,不修改原数据。
函数式风格渗透到底
var大量使用,类型从右侧推演- 早期返回(early return)替代
else map/filter/fold优于for循环readonly struct+[StructLayout(LayoutKind.Sequential)]无处不在Debug.Assert广泛用于不变量验证
三、Plato:用代码生成器构建数学王国
这是 Ara3D 最独特的技术决策之一。Plato 是一个独立的代码生成器(有独立的仓库),它为 SDK 生成了所有数学和几何类型。打开 src/Plato.Generated/,你会看到 100+ 个 .g.cs 文件,全部标记着 // Autogenerated file: DO NOT EDIT。
生成的不仅仅是对 System.Numerics 的封装
每个生成的类型都遵循严格的结构模板,而这套模板的设计异常精巧:
csharp
// Vector3 的生成代码(节选)
[StructLayout(LayoutKind.Sequential, Pack=1)]
public partial struct Vector3 : IVector<Vector3>
{
// 与 C# 元组的隐式双向转换
public static implicit operator (Number, Number, Number)(Vector3 self) => ...;
// 所有 6 个分量置换自动生成
public Vector3 XZY { get => (this.X, this.Z, this.Y); }
// 签名距离场函数直接生成在 Vector3 上!
public Number SdSphere(Number r) => this.Length.Subtract(r);
public Number SdBox(Vector3 b) { ... }
// 贝塞尔曲线及其导数展开
public Vector3 CubicBezier(Vector3 b, Vector3 c, Vector3 d, Number t) => ...;
}
类型的泛型层次
Plato 生成的接口层次是精心设计的:
IValue → IVectorLike → INumerical → IVector → Vector2, Vector3, Vector4
└── IInterpolatable (所有类型支持 Lerp)
└── IScalarArithmetic (向量 × 标量泛型)
这意味着你可以写出这样的泛型算法:
csharp
// 对所有 IVector<T> 类型都生效
public static T LerpToHalf<T>(this T a, T b) where T : IInterpolatable<T>
=> a.Lerp(b, 0.5f);
为什么要自建而不是直接用 System.Numerics?
作者的考量很清晰。System.Numerics 的问题在于:
- 类型不完整 --- 有
Vector3但没有Point3D、TriangleMesh3D等几何概念 - 缺乏统一接口 --- 没有办法对"所有支持加法"的类型写泛型代码
- 版本脆弱性 --- .NET 6/7/8 不同版本间行为变化不可控
- 功能不满足需求 --- 签名距离场、贝塞尔展开、分量置换等功能需要自行实现
Plato 通过代码生成实现了 类型一致 + 接口统一 + 版本锁定 。而且生成的 SdBox、SdTorus 等 SDF 函数直接以实例方法的形式挂在 Vector3 上------这种直觉式的 API 设计,不可能在 System.Numerics 中实现。
四、集合与内存:IReadOnlyList<T> 的一等公民地位
Ara3D 的集合库是整个 SDK 的"血管系统"------所有数据以 IReadOnlyList<T> 形式传递。它回避了 IEnumerable<T> / LINQ to Objects 的惰性求值范式,转而使用一种更激进的模式。
ReadOnlyList<T> 的设计精髓
ReadOnlyList<T> 不是 List<T>------它是一个 函数 + 计数的对偶:
csharp
// ReadOnlyList<T> 只是一个 Func<int, T> 和一个 Count
public readonly struct ReadOnlyList<T> : IReadOnlyList<T>
{
readonly Func<int, T> _getter;
public readonly int Count;
public T this[int index] => _getter(index);
}
关键推导:当 Select 操作应用到这个结构上时,不会分配任何新数组,只创建一个新的函数组合:
csharp
public static ReadOnlyList<U> Select<T, U>(this IReadOnlyList<T> self, Func<T, U> f)
=> self.Count.Select(i => f(self[i])); // 只创建新的 Func<int, U>
这个模式带来了巨大的性能优势------链式变换(Select().Where().Take())没有中间分配,没有任何 IEnumerator 装箱,没有惰性迭代器的状态机。
延伸到多维和稀疏
Ara3D.Collections 还提供了:
IReadOnlyList2D<T>/IReadOnlyList3D<T>--- 通过行优先索引计算的零拷贝多维视图CompressedSparseRow<T>--- CSR 格式稀疏矩阵,在 Topology.cs 中用于半边的邻接查询IntegerRange--- 零分配的整数范围枚举
这个设计理念贯穿了所有 SDK 库------想象一下,一个包含数百万面片的 BIM 模型的几何数据也是以这种零拷贝的方式传递的。
五、几何引擎:从三角剖分到隐式曲面
Ara3D.Geometry 是 SDK 中最大的单一库,覆盖了网格操作、拓扑分析、隐式曲面、三角剖分、空间查询、体素化、形状特征等几乎所有核心几何算法。
精选算法一览
| 领域 | 算法 | 文件 |
|---|---|---|
| 三角剖分 | Bowyer-Watson 2D Delaunay | DelaunayTriangulator.cs |
| 多边形三角化 | Ear-clipping(支持带洞多边形) | PolygonTriangulator.cs |
| 等值面提取 | Marching Cubes | MarchingCubes.cs |
| 隐式曲面 | SDF 函数组合 + 逆变换 | Sdf3D.cs |
| 拓扑 | Half-edge 数据结构 + CSR 邻接 | Topology.cs |
| 空间索引 | AABB Tree | AABBTree.cs |
| 主成分分析 | 协方差矩阵 + 特征分解 | PrincipalComponentAnalysis.cs |
| 形状分类 | >100 个衍生特征(杆状/板状/块状) | MeshFeatures.cs |
一个优雅的 FP 技巧:SDF 变换
csharp
public class Sdf3D : ITransformable3D<Sdf3D>
{
readonly Func<Point3D, Number> _func;
public Number Eval(Point3D p) => _func(p);
// 对 SDF 做变换 = 对采样点做逆变换
public Sdf3D Transform(Transform3D t)
=> new(p => Eval(t.Invert.TransformPoint(p)));
}
不需要修改 SDF 函数本身------只需要对采样点应用逆变换。当你的 SDF 是一个函数组合堆栈时,这种数学上的优雅直接转化为代码的简洁。
半边拓扑 + CSR 矩阵
Topology.cs 从三角形网格构建完整的 half-edge 结构,使用 CSR 矩阵存储顶点-面-边的邻接关系。这种数据结构支持高效的边界环检测、流形判断和欧拉特征数计算------对于 BIM 元素的空间关系分析至关重要。
六、Bowerbird:运行时 C# 编译的命令系统
Bowerbird 是整个生态中最有创意的系统。它把"插件"这个概念从 编译时 搬到了 运行时。
核心思路
CommandsRoot/
├── ExportGeometry/
│ ├── ExportGeometry.manifest.json ← JSON 描述文件
│ ├── ExportGeometryCommand.cs ← 纯 C# 源码
│ └── bin/
│ └── ExportGeometry-20260715T120000.dll ← Roslyn 编译产物
Bowerbird 的工作流程:
CommandCatalog扫描文件夹,读 JSON manifest(命令名、描述、类型名)CommandCompiler调用Microsoft.CodeAnalysis.CSharp编译.cs源码为 DLL- 通过 指纹缓存(源文件哈希 + 引用文件指纹)避免无变化的重复编译
CommandLoader用Assembly.LoadFile加载 DLL,反射创建INamedCommand实例CommandRunner执行命令,返回结果
应用场景
- Ara3D Studio 的脚本系统:用户写 C# 脚本 → 编译 → 执行,无需完整的 VS 项目
- Revit 2025 插件 :
BowerbirdExternalCommand让 Revit 内部也能运行动态编译的 C# 命令 - 命令行工具 :
Ara3D.Bowerbird.Console提供 CLI 入口
csharp
// 一个典型的 Bowerbird 命令
public class ExportGeometryCommand : NamedCommand
{
public override string Name => "export-geometry";
public override void Execute(ICommandContext context)
{
// 获取当前 Revit 文档/3D 场景
// 导出为 PLY/GLTF/BFAST
}
}
这种设计允许终端用户(Revit 建筑师、Studio 使用者)在不重新编译任何东西的前提下扩展应用行为------可以做到"写完即用"。
七、BIM 数据模型与列式存储
BOS(BIM Open Schema)是 Ara3D 定义的工具无关 BIM 数据标准化格式,设计目标非常明确:面向分析优化,而不是面向显示优化。
列式几何存储
BimGeometry 将几何数据组织为 6 个平行数组(struct of arrays):
Instance Table: EntityIndex, MaterialIndex, MeshIndex, TransformIndex
Vertex Table: float[] X, Y, Z (各列独立存储)
Index Table: int[] 三角形索引
Mesh Table: MeshVertexOffset[], MeshIndexOffset[]
Material Table: Red[], Green[], Blue[], Alpha[], Roughness[], Metallic[]
Transform Table: TX[], TY[], TZ[], QX[], QY[], QZ[], QW[], SX[], SY[], SZ[]
为什么用这种模式?因为列式存储直接适配 DuckDB / Apache Parquet 的查询引擎------你可以用 SQL 对几何数据做聚合分析,比如"统计所有 MEP 元素的总体积"或"找出每个楼层所有窗户的面积"。
EAV 参数模型 + 15 种关系
参数数据使用 Entity-Attribute-Value 模式------高度稀疏的 BIM 数据不适合扁平表,EAV 是最自然的建模方式。关系层面定义了 15 种语义关系(PartOf, HostedBy, Voids, Fills, ConnectsTo, BoundedBy 等),同时覆盖 Revit 和 IFC 的语义。
八、MCP:当 3D 工具遇见 AI
在 wip/Ara3D.MCP 中,我们发现了一个零外部依赖的 MCP(Model Context Protocol)实现。这可能是整个项目中最"前沿"的部分。
MCP 是什么,为什么在这个项目里?
MCP 是 Anthropic 提出的开放协议,允许 AI 助手通过标准化接口与外部工具交互。Ara3D 的 MCP 实现让 AI(Cursor、Claude 等)可以直接操控 3D 场景。
csharp
var server = new McpServer("Ara3D Studio", "1.0.0", "Ara3D 3D Studio Tools");
// 注册一个工具
server.Tool("list_entities", "列出场景中所有实体",
args => {
var category = args.GetString("category");
return Results.Ok(entities.Where(e => e.Category == category));
});
server.Start("http://127.0.0.1:8766/mcp/");
与 Bowerbird 的深度绑定
ScriptMcpTools.Refresh() 会扫描 Bowerbird 加载的所有插件,为每个脚本自动注册一个 script_{name} MCP 工具。这就形成了完整的链路:
用户写 C# 脚本 → Bowerbird 编译 → MCP 暴露为 AI 工具 → Cursor/Claude 通过自然语言调用
Bowerbird 的 REFACTOR.md 中也明确写道:"We will use the system for compiling MCP tools"------这说明 MCP 正是 Bowerbird 的下一个演进方向。
九、单人维护的工程智慧
遍历整个项目后,有些工程实践值得单独提炼出来:
1. 技术债务看得见
项目中有一整套债务追踪系统:代码中的 // TODO: 标记 + docs/TECHNICAL_DEBT.md 的条目 + docs/TODO.md 的优先级排序。不藏污纳垢,也第一时间修所有东西------而是记录、排序、在合适的时机处理。这对于单人项目是至关重要的自我管理方法。
2. 分层测试,按需执行
bat
test.bat fast # 跳过 I/O 密集型测试,适合高频迭代
test.bat geometry # 只跑几何模块
test.bat ifcmesher # IFC 网格化专项
test.bat # 完整运行(包含 Slow 测试)
还有一个 tests/Ara3D.SDK.KnownIssues.Tests 项目------专门存放"已知会失败但尚不阻塞开发"的测试用例。当 bug 修好时,测试从 KnownIssues 移回正常项目。这种明确的用测试承载技术债务的做法,在商业化项目中也少见。
3. 度量驱动的 IFC Mesher
wip/Ara3D.Ifc.Mesher 的开发方式很值得学习:每次改动都通过 parity score 与 web-ifc 的基准输出做对比:
- T0: 微测试(单个几何操作)
- T1: 快速评分(典型文件)
- T2: 深度对比(全部几何细节)
- T3: 全量目录扫描
这不是"先写完再修 bug"的模式,而是用度量导航开发。每个 WP(工作包)完成时都有一个可量化的质量指标。
4. 零依赖的价值被反复证明
核心库(Collections、Geometry、Utils)没有引入任何 NuGet 包------只有 .NET 8 BCL。这意味着不会出现"NuGet 整活导致整个 SDK 编译不过"的问题,也意味着每个文件都可以被独立编译和理解。
十、总结:一个"小而全"的标杆
Ara3D SDK 不是一个用来炫耀技术广度的玩具项目。它是一个已经跑在生产环境(NuGet 发布、Studio 使用、Revit 集成)的真实产品。它的存在证明了几个重要的命题:
-
代码生成不是过度工程------当领域模型足够复杂(100+ 类型、200+ 曲线/形状),代码生成器是唯一能保证一致性和可维护性的方案。
-
IReadOnlyList<T>+ 扩展方法 这种 FP 模式在 C# 中远比想象中强大------它提供了 LINQ 的表达力,却避免了IEnumerable的分配开销和惰性求值的副作用。 -
单人项目可以做到"大而全"------只要坚持零外部依赖、用代码生成降低维护成本、用度量驱动质量、并诚实地记录技术债务。
-
MCP + Bowerbird 的组合展示了 3D 工具与 AI 的集成方向------用户写的 C# 脚本自动暴露为 AI 工具,这是自然语言操控 3D 软件的可行路径。