核心设计哲学:FP + 不可变 + 接口扩展
翻阅项目的 AGENTS.md,两条原则凌驾于一切之上:
代码必须能工作。 正确性优先。一个能工作的简单版本胜过坏掉的复杂版本。
代码必须易于改进和重构。 所有其他原则服务于这个目标。
这是典型的 现实主义工程哲学。作者没有追求"企业级抽象"和"最佳实践"的教条,而是选择了一条更务实、也更具个人风格的路线。具体的代码准则包括:
不可变扩展函数 + 最小接口
这是整个 SDK 中最高频出现的模式。看看这个:
// 小接口,行为通过扩展函数添加
public static bool InRange(this IReadOnlyList 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 的封装
每个生成的类型都遵循严格的结构模板,而这套模板的设计异常精巧:
// Vector3 的生成代码(节选)
[StructLayout(LayoutKind.Sequential, Pack=1)]
public partial struct Vector3 : IVector
{
// 与 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 (向量 × 标量泛型)
这意味着你可以写出这样的泛型算法:
// 对所有 IVector 类型都生效
public static T LerpToHalf(this T a, T b) where T : IInterpolatable
=> 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 的一等公民地位
Ara3D 的集合库是整个 SDK 的"血管系统"——所有数据以 IReadOnlyList 形式传递。它回避了 IEnumerable / LINQ to Objects 的惰性求值范式,转而使用一种更激进的模式。
ReadOnlyList 的设计精髓
ReadOnlyList 不是 List——它是一个 函数 + 计数的对偶:
// ReadOnlyList 只是一个 Func<int, T> 和一个 Count
public readonly struct ReadOnlyList : IReadOnlyList
{
readonly Func<int, T> _getter;
public readonly int Count;
public T this[int index] => _getter(index);
}
关键推导:当 Select 操作应用到这个结构上时,不会分配任何新数组,只创建一个新的函数组合:
public static ReadOnlyListSelect<T, U>(this IReadOnlyList self, Func<T, U> f)
=> self.Count.Select(i => f(self[i])); // 只创建新的 Func<int, U>
这个模式带来了巨大的性能优势——链式变换(Select().Where().Take())没有中间分配,没有任何 IEnumerator 装箱,没有惰性迭代器的状态机。
延伸到多维和稀疏
Ara3D.Collections 还提供了:
IReadOnlyList2D / IReadOnlyList3D — 通过行优先索引计算的零拷贝多维视图
CompressedSparseRow — 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 变换
public class Sdf3D : ITransformable3D
{
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 入口
// 一个典型的 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 场景。
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 的下一个演进方向。
九、单人维护的工程智慧
遍历整个项目后,有些工程实践值得单独提炼出来:
技术债务看得见
项目中有一整套债务追踪系统:代码中的 // TODO: 标记 + docs/TECHNICAL_DEBT.md 的条目 + docs/TODO.md 的优先级排序。不藏污纳垢,也第一时间修所有东西——而是记录、排序、在合适的时机处理。这对于单人项目是至关重要的自我管理方法。分层测试,按需执行
test.bat fast # 跳过 I/O 密集型测试,适合高频迭代
test.bat geometry # 只跑几何模块
test.bat ifcmesher # IFC 网格化专项
test.bat # 完整运行(包含 Slow 测试)
还有一个 tests/Ara3D.SDK.KnownIssues.Tests 项目——专门存放"已知会失败但尚不阻塞开发"的测试用例。当 bug 修好时,测试从 KnownIssues 移回正常项目。这种明确的用测试承载技术债务的做法,在商业化项目中也少见。度量驱动的 IFC Mesher
wip/Ara3D.Ifc.Mesher 的开发方式很值得学习:每次改动都通过 parity score 与 web-ifc 的基准输出做对比:
T0: 微测试(单个几何操作)
